]> sipb.mit.edu Git - ikiwiki.git/blob - doc/patchqueue/calendar_--_archive_browsing_via_a_calendar_frontend.mdwn
formatting
[ikiwiki.git] / doc / patchqueue / calendar_--_archive_browsing_via_a_calendar_frontend.mdwn
1 This plugin is inspired by the calendar plugin for Blosxom, but derivesno code from it. This plugin is essentially a fancy front end to archives of previous pages, usually used for blogs. It can produce a calendar for a given month, or a list of months for a given year. To invoke the calendar, just use the preprocessor directive:
2
3      \[[calendar ]]
4
5 or
6
7       \[[calendar type="month" pages="blog/* and !*/Discussion"]]
8
9 or
10
11        \[[calendar type="year"  year="2005" pages="blog/* and !*/Discussion"]]
12
13
14 The year and month entities in the out put have links to archive index pages, which are supposed to exist already. The idea is to create an archives hierarchy, rooted in the subdirectory specified in the site-wide customization variable, archivebase. archivebase defaults to "archives".  Links are created to pages "$archivebase/$year" and "$archivebase/$year/$month". The idea is to create annual and monthly indices, for example, by using something like this sample from my archives/2006/01.mdwn 
15
16           \[[meta title="Archives for 2006/01"]]
17           \[[inline rootpage="blog" atom="no" rss="no" show="0" pages="blog/* and !*/Discussion and creation_year(2006) and creation_month(01)" ]]
18
19 I'll send in the patch via email.
20
21
22 ManojSrivastava
23
24 ------
25
26 Since this is a little bit er, stalled, I'll post here the stuff Manoj
27 mailed me, and my response to it. --[[Joey]]
28
29 <pre>
30 #! /usr/bin/perl
31 #                              -*- Mode: Cperl -*- 
32 # calendar.pm --- 
33 # Author           : Manoj Srivastava ( srivasta@glaurung.internal.golden-gryphon.com ) 
34 # Created On       : Fri Dec  8 16:05:48 2006
35 # Created On Node  : glaurung.internal.golden-gryphon.com
36 # Last Modified By : Manoj Srivastava
37 # Last Modified On : Sun Dec 10 01:53:22 2006
38 # Last Machine Used: glaurung.internal.golden-gryphon.com
39 # Update Count     : 139
40 # Status           : Unknown, Use with caution!
41 # HISTORY          : 
42 # Description      : 
43
44 # arch-tag: 2aa737c7-3d62-4918-aaeb-fd85b4b1384c
45 #
46 # Copyright (c) 2006 Manoj Srivastava <srivasta@debian.org>
47 #
48 # This program is free software; you can redistribute it and/or modify
49 # it under the terms of the GNU General Public License as published by
50 # the Free Software Foundation; either version 2 of the License, or
51 # (at your option) any later version.
52 #
53 # This program is distributed in the hope that it will be useful,
54 # but WITHOUT ANY WARRANTY; without even the implied warranty of
55 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
56 # GNU General Public License for more details.
57 #
58 # You should have received a copy of the GNU General Public License
59 # along with this program; if not, write to the Free Software
60 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
61 #
62
63 require 5.002;
64 package IkiWiki::Plugin::calendar;
65
66 use warnings;
67 use strict;
68 use IkiWiki '1.00';
69 use Time::Local;
70
71 our $VERSION = "0.1";
72 my $file = __FILE__;
73
74 my %calpages;
75 my %cache;
76 my %linkcache;
77
78 my $index=1;
79 my @now=localtime();
80
81 =head1 NAME
82
83 calendar - Add links for the current month's, current year's, and older archived postings
84
85 =cut
86
87 =head1 SYNOPSIS
88
89 To invoke the calendar, just use the preprocessor directive (options
90 and variations are detailed below):
91
92   [[calendar ]]
93
94 or
95
96   [[calendar type="month" pages="blog/* and !*/Discussion"]]
97
98 or
99
100   [[calendar type="year"  year="2005" pages="blog/* and !*/Discussion"]]
101
102 =cut
103
104
105 =head1 DESCRIPTION
106
107 This plugin is inspired by the calendar plugin for Blosxom, but
108 derives no code from it. This plugin is essentially a fancy front end
109 to archives of previous pages, usually used for blogs. It can produce
110 a calendar for a given month, or a list of months for a given year. 
111
112 The year and month entities in the out put have links to archive index
113 pages, which are supposed to exist already. The idea is to create an
114 archives hierarchy, rooted in the subdirectory specified in the
115 site wide customization variable, I<archivebase>. I<archivebase>
116 defaults to C<archives>.  Links are created to pages
117 C<$archivebase/$year> and C<$archivebase/$year/$month>. If one creates
118 annual and monthly indices, for example, by using something like this
119 sample from my I<archives/2006/01.mdwn> (warning: line split for
120 readability):
121
122    \[[meta title="Archives for 2006/01"]]
123    \[[inline rootpage="blog" atom="no" rss="no" show="0"
124      pages="blog/* and !*/Discussion and creation_year(2006)
125             and creation_month(01)"
126    ]]
127
128 =cut
129
130 =head1 OPTIONS
131
132 =over
133
134 =item B<type>
135
136 Used to specify the type of calendar wanted. Can be one of C<month> or
137 C<year>. The default is a month view calendar.
138
139 =item B<pages>
140
141 Specifies the C<pagespec> used to get pages to match for
142 linking. Usually this should be something like C<blog/* and !*/Discussion>.
143 Defaults to C<*>.
144
145 =item B<year>
146
147 The year for which the calendar is requested. Defaults to the current year.
148
149 =item B<month>
150
151 The numeric month for which the calendar is requested, in the range
152 1..12. Used only for the month view calendar, and defaults to the
153 current month.
154
155 =item B<week_start_day>
156
157 A number, in the range 0..6, which represents the day of the week that
158 the month calendar starts with. 0 is Sunday, 1 is Monday, and so
159 on. Defaults to 0, which is Sunday.
160
161 =item B<months_per_row>
162
163 In the annual calendar, number of months to place in each row. Defaults to 3.
164
165 =back
166
167 =cut
168
169 =head1 Classes for CSS control
170
171 The output is liberally sprinkled with classes, for fine grained CSS
172 customization.
173
174 =over
175
176 =item C<month-calendar> 
177
178 The month calendar as a whole
179
180 =item C<month-calendar-head>
181
182 The head of the month calendar (ie,"March"), localized to the environment.
183
184 =item C<month-calendar-day-head>
185
186 A column head in the month calendar (ie, a day-of-week abbreviation),
187 localized.
188
189 =item C<month-calendar-day-noday>, C<month-calendar-day-link>,
190   C<month-calendar-day-nolink>, C<month-calendar-day-future>,
191   C<month-calendar-day-this-day> 
192
193 The day squares on the month calendar, for days that don't exist
194 (before or after the month itself), that don't have stories, that do
195 have stories, that are in the future, or are that currently selected,
196 respectively (today).
197
198 =item Day-of-week-name
199
200 Each day square is also given a class matching its day of week, this
201 can be used to high light weekends. This is also localized.
202
203 =item C<year-calendar>
204
205 The year calendar as a whole
206
207 =item C<year-calendar-head>
208
209 The head of the year calendar (ie, "2006")
210
211 =item C<year-calendar-subhead>
212
213 For example, "Months"
214
215 =item C<year-calendar-month-link>, C<year-calendar-month-nolink>,
216   C<year-calendar-month-future>, C<year-calendar-this-month>
217
218 The month squares on the year calendar, for months with stories,
219 without, in the future, and currently selected, respectively.
220
221 =back
222
223 =cut
224
225
226 sub import {
227   hook(type => "preprocess", id => "calendar", call => \&preprocess);
228   hook(type => "format", id => "calendar", call => \&format);
229 }
230
231 sub preprocess (@) {
232   my %params=@_;
233   $params{pages} = "*"            unless defined $params{pages};
234   $params{type}  = "month"        unless defined $params{type};
235   $params{year}  = 1900 + $now[5] unless defined $params{year};
236   $params{month} = sprintf("%02d", $params{month}) if defined  $params{month};
237   $params{month} = 1    + $now[4] unless defined $params{month};
238   $params{week_start_day} = 0     unless defined $params{week_start_day};
239   $params{months_per_row} = 3     unless defined $params{months_per_row};
240
241   # Store parameters (could be multiple calls per page)
242   $calpages{$params{destpage}}{$index} = \%params;
243
244   return "\n<div class=\"calendar\">" . $index++ . "</div><!-- calendar -->\n";
245 }
246
247 sub is_leap_year (@) {
248   my %params=@_;
249   return ($params{year} % 4 == 0 && (($params{year} % 100 != 0) || $params{year} % 400 ==0)) ;
250 }
251
252
253 sub month_days {
254   my %params=@_;
255   my $days_in_month = (31,28,31,30,31,30,31,31,30,31,30,31)[$params{month}-1];
256   if ($params{month} == 2 && is_leap_year(%params)) {
257     $days_in_month++;
258   }
259   return $days_in_month;
260 }
261
262
263 sub format_month (@) {
264   my %params=@_;
265   my $pagespec = $params{pages};
266   my $year     = $params{year};
267   my $month    = $params{month};
268
269   my $calendar="\n";
270
271   # When did this month start?
272   my @monthstart = localtime(timelocal(0,0,0,1,$month-1,$year-1900));
273
274   my $future_dom = 0;
275   my $today      = 0;
276   $future_dom = $now[3]+1 if ($year == $now[5]+1900 && $month == $now[4]+1);
277   $today      = $now[3]   if ($year == $now[5]+1900 && $month == $now[4]+1);
278
279   # Calculate month names for next month, and previous months
280   my $pmonth = $month - 1;
281   my $nmonth = $month + 1;
282   my $pyear  = $year;
283   my $nyear  = $year;
284
285   # Adjust for January and December
286   if ($month == 1)  { $pmonth = 12; $pyear--;  }
287   if ($month == 12) { $nmonth = 1;  $nyear++;  }
288
289   # Find out month names for this, next, and previous months
290   my $monthname=POSIX::strftime("%B", @monthstart);
291   my $pmonthname=
292     POSIX::strftime("%B", localtime(timelocal(0,0,0,1,$pmonth-1,$pyear-1900)));
293   my $nmonthname=
294     POSIX::strftime("%B", localtime(timelocal(0,0,0,1,$nmonth-1,$nyear-1900)));
295
296   # Calculate URL's for monthly archives, and article counts
297   my $archivebase = 'archives';
298   $archivebase = $config{archivebase} if defined $config{archivebase};
299
300   my ($url, $purl, $nurl)=("$monthname",'','');
301   my ($count, $pcount, $ncount) = (0,0,0);
302
303   if (exists $cache{$pagespec}{"$year/$month"}) {
304     $url = htmllink($params{page}, $params{destpage}, 
305                     "$archivebase/$year/" . sprintf("%02d", $month),
306                     0,0," $monthname ");
307   }
308
309   if (exists $cache{$pagespec}{"$pyear/$pmonth"}) {
310     $purl = htmllink($params{page}, $params{destpage}, 
311                      "$archivebase/$pyear/" . sprintf("%02d", $pmonth),
312                      0,0," $pmonthname ");
313   }
314   if (exists $cache{$pagespec}{"$nyear/$nmonth"}) {
315     $nurl = htmllink($params{page}, $params{destpage}, 
316                      "$archivebase/$nyear/" . sprintf("%02d", $nmonth),
317                      0,0," $nmonthname ");
318   }
319
320   # Start producing the month calendar
321   $calendar=<<EOF;
322 <table class="month-calendar">
323    <caption class="month-calendar-head">
324       $purl
325       $url
326       $nurl
327    </caption>
328    <tr>
329 EOF
330   # Suppose we want to start the week with day $week_start_day
331   # If $monthstart[6] == 1
332   my $week_start_day = $params{week_start_day};
333
334   my $start_day = 1 + (7 - $monthstart[6] + $week_start_day) % 7;
335   my %downame;
336   my %dowabbr;
337   for my $dow ($week_start_day..$week_start_day+6) {
338     my @day=localtime(timelocal(0,0,0,$start_day++,$month-1,$year-1900));
339     my $downame = POSIX::strftime("%A", @day);
340     my $dowabbr = POSIX::strftime("%a", @day);
341     $downame{$dow % 7}=$downame;
342     $dowabbr{$dow % 7}=$dowabbr;
343     $calendar.=
344       qq{     <th class="month-calendar-day-head $downame">$dowabbr</th>\n};
345   }
346
347   $calendar.=<<EOF;
348    </tr>
349 EOF
350
351   my $wday;
352   # we start with a week_start_day, and skip until we get to the first
353   for ($wday=$week_start_day; $wday != $monthstart[6]; $wday++, $wday %= 7) {
354     $calendar.=qq{   <tr>\n} if $wday == $week_start_day;
355     $calendar.=
356       qq{     <td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
357   }
358
359   # At this point, either the first is a week_start_day, in which case nothing
360   # has been printed, or else we are in the middle of a row.
361   for (my $day = 1; $day <= month_days(year => $year, month => $month); 
362        $day++, $wday++, $wday %= 7) {
363     # At tihs point, on a week_start_day, we close out a row, and start a new
364     # one -- unless it is week_start_day on the first, where we do not close a
365     # row -- since none was started.
366     if ($wday == $week_start_day) {
367       $calendar.=qq{   </tr>\n} unless $day == 1;
368       $calendar.=qq{   <tr>\n};
369     }
370     my $tag;
371     my $mtag = sprintf("%02d", $month);
372     if (defined $cache{$pagespec}{"$year/$mtag/$day"}) {
373       if ($day == $today)         { $tag='month-calendar-day-this-day'; }
374       else                        { $tag='month-calendar-day-link';     }
375       $calendar.=qq{     <td class="$tag $downame{$wday}">};
376       $calendar.=
377         htmllink($params{page}, $params{destpage}, 
378                  pagename($linkcache{"$year/$mtag/$day"}),
379                  0,0,"$day");
380       $calendar.=qq{</td>\n};
381     }
382     else {
383       if ($day == $today)         { $tag='month-calendar-day-this-day'; }
384       elsif ($day == $future_dom) { $tag='month-calendar-day-future';   }
385       else                        { $tag='month-calendar-day-nolink';   }
386       $calendar.=qq{     <td class="$tag $downame{$wday}">$day</td>\n};
387     }
388   }
389   # finish off the week
390   for (; $wday != $week_start_day; $wday++, $wday %= 7) {
391     $calendar.=qq{     <td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
392   }
393   $calendar.=<<EOF;
394    </tr>
395 </table>
396 EOF
397
398   return $calendar;
399 }
400
401 sub format_year (@) {
402   my %params=@_;
403   my $pagespec = $params{pages};
404   my $year     = $params{year};
405   my $month    = $params{month};
406   my $calendar="\n";
407   my $pyear  = $year - 1;
408   my $nyear  = $year + 1;
409   my $future_month = 0;
410   $future_month = $now[4]+1 if ($year == $now[5]+1900);
411
412   #  calculate URL's for previous and next years
413   my $archivebase = 'archives';
414   $archivebase = $config{archivebase} if defined $config{archivebase};
415   my ($url, $purl, $nurl)=("$year",'','');
416   if (exists $cache{$pagespec}{"$year"}) {
417     $url = htmllink($params{page}, $params{destpage}, 
418                     "$archivebase/$year",
419                     0,0,"$year");
420   }
421
422   if (exists $cache{$pagespec}{"$pyear"}) {
423     $purl = htmllink($params{page}, $params{destpage}, 
424                      "$archivebase/$pyear",
425                      0,0,"\&larr;");
426   }
427   if (exists $cache{$pagespec}{"$nyear"}) {
428     $nurl = htmllink($params{page}, $params{destpage}, 
429                      "$archivebase/$nyear",
430                      0,0,"\&rarr;");
431   }
432   # Start producing the year calendar
433   $calendar=<<EOF;
434 <table class="year-calendar">
435   <caption class="year-calendar-head">
436    $purl
437    $url
438    $nurl
439   </caption>
440   <tr>
441     <th class="year-calendar-subhead" colspan="$params{months_per_row}">Months</th>
442   </tr>
443 EOF
444
445   for ($month = 1; $month <= 12; $month++) {
446     my @day=localtime(timelocal(0,0,0,15,$month-1,$year-1900));
447     my $murl;
448     my $monthname = POSIX::strftime("%B", @day);
449     my $monthabbr = POSIX::strftime("%b", @day);
450     $calendar.=qq{   <tr>\n}  if ($month % $params{months_per_row} == 1);
451     my $tag;
452     my $mtag=sprintf("%02d", $month);
453     if ($month == $params{month}) {
454       if ($cache{$pagespec}{"$year/$mtag"})        {$tag = 'this_month_link'}
455       else                                         {$tag = 'this_month_nolink'}
456     }
457     elsif ($cache{$pagespec}{"$year/$mtag"})       {$tag = 'month_link'} 
458     elsif ($future_month && $month >=$future_month){$tag = 'month_future'} 
459     else                                           {$tag = 'month_nolink'}
460     if ($cache{$pagespec}{"$year/$mtag"}) {
461       $murl =  htmllink($params{page}, $params{destpage}, 
462                         "$archivebase/$year/$mtag",
463                         0,0,"$monthabbr");
464       $calendar.=qq{     <td class="$tag">};
465       $calendar.=$murl;
466       $calendar.=qq{</td>\n};
467     }
468     else {
469       $calendar.=qq{     <td class="$tag">$monthabbr</td>\n};
470     }
471     $calendar.=qq{   </tr>\n} if ($month % $params{months_per_row} == 0);
472   }
473   $calendar.=<<EOF;
474 </table>
475 EOF
476
477   return $calendar;
478 }
479
480
481 sub format (@) {
482   my %params=@_;
483   my $content=$params{content};
484   return $content unless exists $calpages{$params{page}};
485
486   # Restore parameters for each invocation
487   foreach my $index (keys %{$calpages{$params{page}}}) {
488     my $calendar="\n";
489     my %saved = %{$calpages{$params{page}}{$index}};
490     my $pagespec=$saved{pages};
491
492     if (! defined $cache{$pagespec}) {
493       for my $page (sort keys %pagesources) {
494         next unless pagespec_match($page,$pagespec);
495         my $mtime;
496         my $src = $pagesources{$page};
497         if (! exists $IkiWiki::pagectime{$page}) {
498           $mtime=(stat(srcfile($src)))[9];
499         }
500         else {
501           $mtime=$IkiWiki::pagectime{$page}
502         }
503         my @date  = localtime($mtime);
504         my $mday  = $date[3];
505         my $month = $date[4] + 1;
506         my $year  = $date[5] + 1900;
507         my $mtag  = sprintf("%02d", $month);
508         $linkcache{"$year/$mtag/$mday"} = "$src";
509         $cache{$pagespec}{"$year"}++;
510         $cache{$pagespec}{"$year/$mtag"}++;
511         $cache{$pagespec}{"$year/$mtag/$mday"}++;
512       }
513     }
514     # So, we have cached data for the current pagespec at this point
515     if ($saved{type} =~ /month/i) {
516       $calendar=format_month(%saved);
517     }
518     elsif ($saved{type} =~ /year/i) {
519       $calendar=format_year(%saved);
520     }
521     $content =~ s/(<div class=\"calendar\">\s*.?\s*$index\b)/<div class=\"calendar\">$calendar/ms;
522   }
523   return $content;
524 }
525
526
527
528 =head1 CAVEATS
529
530 In the month calendar, for days in which there is more than one
531 posting, the link created randomly selects one of them. Since there is
532 no easy way in B<IkiWiki> to automatically generate index pages, and
533 pregenerating daily index pages seems too much of an overhead, we have
534 to live with this.  All postings can still be viewed in the monthly or
535 annual indices, of course. This can be an issue for very prolific
536 scriveners.
537
538 =cut
539
540 =head1 BUGS
541
542 None Known so far.
543
544 =head1 BUGS
545
546 Since B<IkiWiki> eval's the configuration file, the values have to all
547 on a single physical line. This is the reason we need to use strings
548 and eval, instead of just passing in real anonymous sub references,
549 since the eval pass converts the coderef into a string of the form
550 "(CODE 12de345657)" which can't be dereferenced.
551
552 =cut
553
554 =head1 AUTHOR
555
556 Manoj Srivastava <srivasta@debian.org>
557
558 =head1 COPYRIGHT AND LICENSE
559
560 This script is a part of the Devotee package, and is 
561
562 Copyright (c) 2002 Manoj Srivastava <srivasta@debian.org>
563
564 This program is free software; you can redistribute it and/or modify
565 it under the terms of the GNU General Public License as published by
566 the Free Software Foundation; either version 2 of the License, or
567 (at your option) any later version.
568
569 This program is distributed in the hope that it will be useful,
570 but WITHOUT ANY WARRANTY; without even the implied warranty of
571 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
572 GNU General Public License for more details.
573
574 You should have received a copy of the GNU General Public License
575 along with this program; if not, write to the Free Software
576 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
577
578 =cut
579
580 1;
581
582 __END__
583 </pre>
584
585 ------
586
587 I've been looking over the calendar plugin. Some items:
588
589 * Why did you need to use a two-stage generation with a format hook?
590   That approach should only be needed if adding something to a page that
591   would be removed by the htmlscrubber, and as far as I can tell, the
592   calendars don't involve anything that would be a problem. It seems
593   that emitting the whole calendar in the preprocess hook would simplify
594   things and you'd not need to save state about calendars.
595
596 > I am scared of the html scrubber, and have never turned it on,
597 >        and did not look too deeply into what would be scrubbed out --ManojSrivastava
598 >> Unless you're using javascript, a few annoyances link <blink>, or inline
599 >> css, it's unlikly to object to any html you might write. The list of
600 >> allowed tags and attributes is easy to find near the top of the plugin.
601
602 > In case the option that gets the ctime of the pages from the
603 > SCM itself, %IkiWiki::pagectime  is not populated that early,
604 > is it? So I waited until the last possible moment to look at
605 > the time information.
606 >
607 >> Actually, since my big rewrite of the rendering path a few months ago,
608 >> ikiwiki scans and populates almost all page information before starting
609 >> to render any page. This includes %pagectime, and even %links. So you
610 >> shouldn't need to worry about running it late.
611
612 * The way that it defaults to the current year and current month
613   is a little bit tricky, because of course the wiki might not get
614   updated in a particular time period, and even if it is updated, only
615   iff a page containing a calendar is rebuilt for some other reason will
616   the calendar get updated, and change what year or month it shows. This
617   is essentially the same problem described in
618   [[todo/tagging_with_a_publication_date]],
619   although I don't think it will affect the calendar plugin very badly.
620   Still, the docs probably need to be clear about this.
621
622 > I use it on the sidebar; and the blog pages are almost always
623 > rebuilt, which is where the calendar is  looked at most often. Oh,
624 > and I also cheat, I have ikiwiki  --setup foo as a @daily cronjob, so
625 > my wiki is always built daily from scratch.
626
627 > I think it should be mentioned, yes.
628
629 * There seems to be something a bit wrong with the year-to-year
630   navigation in the calendar, based on the example in your blog. If I'm
631   on the page for 2006, there's an arrow pointing left which takes me to
632   2005. If I'm on 2005, the arrow points left, but goes to 2006, not
633   2004.
634
635 > I need to look into this.
636
637 * AIUI, the archivebase setting makes a directory rooted at the top of
638   the wiki, so you can have only one set of archives per wiki, in
639   /archives/. It would be good if it were possible to have multiple
640   archived for different blogs in the same wiki at multiple locations.
641   Though since the archives contain calendars, the archive location
642   can't just be relative to the page with the calendar. But perhaps
643   archivebase could be a configurable parameter that can be specified in
644   the directive for the calendar? (It would be fine to keep the global
645   location as a default.)
646
647 > OK, this is simple enough to implement. I'll do that (well,
648 > perhaps not before Xmas, I have a family dinner to cook) and send in
649 > another patch.
650
651
652 ----
653
654 And that's all I've heard so far. Hoping I didn't miss another patch?
655
656 --[[Joey]]