aed087eed58248d5da153543dba03abba17b5ef0
[ikiwiki.git] / IkiWiki / Plugin / calendar.pm
1 #! /usr/bin/perl
2 # Copyright (c) 2006, 2007 Manoj Srivastava <srivasta@debian.org>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18 require 5.002;
19 package IkiWiki::Plugin::calendar;
20
21 use warnings;
22 use strict;
23 use IkiWiki 2.00;
24 use Time::Local;
25 use POSIX;
26
27 my %cache;
28 my %linkcache;
29 my $time=time;
30 my @now=localtime($time);
31
32 sub import { #{{{
33         hook(type => "needsbuild", id => "version", call => \&needsbuild);
34         hook(type => "preprocess", id => "calendar", call => \&preprocess);
35 } #}}}
36
37 sub is_leap_year (@) { #{{{
38         my %params=@_;
39         return ($params{year} % 4 == 0 && (($params{year} % 100 != 0) || $params{year} % 400 == 0));
40 } #}}}
41
42 sub month_days { #{{{
43         my %params=@_;
44         my $days_in_month = (31,28,31,30,31,30,31,31,30,31,30,31)[$params{month}-1];
45         if ($params{month} == 2 && is_leap_year(%params)) {
46                 $days_in_month++;
47         }
48         return $days_in_month;
49 } #}}}
50
51 sub format_month (@) { #{{{
52         my %params=@_;
53
54         my $pagespec = $params{pages};
55         my $year     = $params{year};
56         my $month    = $params{month};
57         my $pmonth   = $params{pmonth};
58         my $nmonth   = $params{nmonth};
59         my $pyear    = $params{pyear};
60         my $nyear    = $params{nyear};
61
62         my @list;
63         my $calendar="\n";
64
65         # When did this month start?
66         my @monthstart = localtime(timelocal(0,0,0,1,$month-1,$year-1900));
67
68         my $future_dom = 0;
69         my $today      = 0;
70         if ($year == $now[5]+1900 && $month == $now[4]+1) {
71                 $future_dom = $now[3]+1;
72                 $today      = $now[3];
73         }
74
75         # Find out month names for this, next, and previous months
76         my $monthname=POSIX::strftime("%B", @monthstart);
77         my $pmonthname=POSIX::strftime("%B", localtime(timelocal(0,0,0,1,$pmonth-1,$pyear-1900)));
78         my $nmonthname=POSIX::strftime("%B", localtime(timelocal(0,0,0,1,$nmonth-1,$nyear-1900)));
79
80         my $archivebase = 'archives';
81         $archivebase = $config{archivebase} if defined $config{archivebase};
82         $archivebase = $params{archivebase} if defined $params{archivebase};
83   
84         # Calculate URL's for monthly archives.
85         my ($url, $purl, $nurl)=("$monthname",'','');
86         if (exists $cache{$pagespec}{"$year/$month"}) {
87                 $url = htmllink($params{page}, $params{destpage}, 
88                         "$archivebase/$year/".sprintf("%02d", $month),
89                         linktext => " $monthname ");
90         }
91         add_depends($params{page}, "$archivebase/$year/".sprintf("%02d", $month));
92         if (exists $cache{$pagespec}{"$pyear/$pmonth"}) {
93                 $purl = htmllink($params{page}, $params{destpage}, 
94                         "$archivebase/$pyear/" . sprintf("%02d", $pmonth),
95                         linktext => " $pmonthname ");
96         }
97         add_depends($params{page}, "$archivebase/$pyear/".sprintf("%02d", $pmonth));
98         if (exists $cache{$pagespec}{"$nyear/$nmonth"}) {
99                 $nurl = htmllink($params{page}, $params{destpage}, 
100                         "$archivebase/$nyear/" . sprintf("%02d", $nmonth),
101                         linktext => " $nmonthname ");
102         }
103         add_depends($params{page}, "$archivebase/$nyear/".sprintf("%02d", $nmonth));
104
105         # Start producing the month calendar
106         $calendar=<<EOF;
107 <table class="month-calendar">
108         <caption class="month-calendar-head">
109         $purl
110         $url
111         $nurl
112         </caption>
113         <tr>
114 EOF
115
116         # Suppose we want to start the week with day $week_start_day
117         # If $monthstart[6] == 1
118         my $week_start_day = $params{week_start_day};
119
120         my $start_day = 1 + (7 - $monthstart[6] + $week_start_day) % 7;
121         my %downame;
122         my %dowabbr;
123         for my $dow ($week_start_day..$week_start_day+6) {
124                 my @day=localtime(timelocal(0,0,0,$start_day++,$month-1,$year-1900));
125                 my $downame = POSIX::strftime("%A", @day);
126                 my $dowabbr = POSIX::strftime("%a", @day);
127                 $downame{$dow % 7}=$downame;
128                 $dowabbr{$dow % 7}=$dowabbr;
129                 $calendar.= qq{\t\t<th class="month-calendar-day-head $downame">$dowabbr</th>\n};
130         }
131
132         $calendar.=<<EOF;
133         </tr>
134 EOF
135
136         my $wday;
137         # we start with a week_start_day, and skip until we get to the first
138         for ($wday=$week_start_day; $wday != $monthstart[6]; $wday++, $wday %= 7) {
139                 $calendar.=qq{\t<tr>\n} if $wday == $week_start_day;
140                 $calendar.=qq{\t\t<td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
141         }
142
143         # At this point, either the first is a week_start_day, in which case
144         # nothing has been printed, or else we are in the middle of a row.
145         for (my $day = 1; $day <= month_days(year => $year, month => $month);
146              $day++, $wday++, $wday %= 7) {
147                 # At tihs point, on a week_start_day, we close out a row,
148                 # and start a new one -- unless it is week_start_day on the
149                 # first, where we do not close a row -- since none was started.
150                 if ($wday == $week_start_day) {
151                         $calendar.=qq{\t</tr>\n} unless $day == 1;
152                         $calendar.=qq{\t<tr>\n};
153                 }
154                 
155                 my $tag;
156                 my $mtag = sprintf("%02d", $month);
157                 if (defined $cache{$pagespec}{"$year/$mtag/$day"}) {
158                         if ($day == $today) {
159                                 $tag='month-calendar-day-this-day';
160                         }
161                         else {
162                                 $tag='month-calendar-day-link';
163                         }
164                         $calendar.=qq{\t\t<td class="$tag $downame{$wday}">};
165                         $calendar.=htmllink($params{page}, $params{destpage}, 
166                                             pagename($linkcache{"$year/$mtag/$day"}),
167                                             "linktext" => "$day");
168                         push @list, pagename($linkcache{"$year/$mtag/$day"});
169                         $calendar.=qq{</td>\n};
170                 }
171                 else {
172                         if ($day == $today) {
173                                 $tag='month-calendar-day-this-day';
174                         }
175                         elsif ($day == $future_dom) {
176                                 $tag='month-calendar-day-future';
177                         }
178                         else {
179                                 $tag='month-calendar-day-nolink';
180                         }
181                         $calendar.=qq{\t\t<td class="$tag $downame{$wday}">$day</td>\n};
182                 }
183         }
184
185         # finish off the week
186         for (; $wday != $week_start_day; $wday++, $wday %= 7) {
187                 $calendar.=qq{\t\t<td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
188         }
189         $calendar.=<<EOF;
190         </tr>
191 </table>
192 EOF
193
194         # Add dependencies to update the calendar whenever pages
195         # matching the pagespec are added or removed.
196         add_depends($params{page}, $params{pages});
197         # Explicitly add all currently linked pages as dependencies, so
198         # that if they are removed, the calendar will be sure to be updated.
199         add_depends($params{page}, join(" or ", @list));
200
201         return $calendar;
202 } #}}}
203
204 sub format_year (@) { #{{{
205         my %params=@_;
206
207         my $pagespec = $params{pages};
208         my $year     = $params{year};
209         my $month    = $params{month};
210         my $pmonth   = $params{pmonth};
211         my $nmonth   = $params{nmonth};
212         my $pyear    = $params{pyear};
213         my $nyear    = $params{nyear};
214
215         my $calendar="\n";
216
217         my $future_month = 0;
218         $future_month = $now[4]+1 if ($year == $now[5]+1900);
219
220         my $archivebase = 'archives';
221         $archivebase = $config{archivebase} if defined $config{archivebase};
222         $archivebase = $params{archivebase} if defined $params{archivebase};
223
224         # calculate URL's for previous and next years
225         my ($url, $purl, $nurl)=("$year",'','');
226         if (exists $cache{$pagespec}{"$year"}) {
227                 $url = htmllink($params{page}, $params{destpage}, 
228                         "$archivebase/$year",
229                         linktext => "$year");
230         }
231         add_depends($params{page}, "$archivebase/$year");
232         if (exists $cache{$pagespec}{"$pyear"}) {
233                 $purl = htmllink($params{page}, $params{destpage}, 
234                         "$archivebase/$pyear",
235                         linktext => "\&larr;");
236         }
237         add_depends($params{page}, "$archivebase/$pyear");
238         if (exists $cache{$pagespec}{"$nyear"}) {
239                 $nurl = htmllink($params{page}, $params{destpage}, 
240                         "$archivebase/$nyear",
241                         linktext => "\&rarr;");
242         }
243         add_depends($params{page}, "$archivebase/$nyear");
244
245         # Start producing the year calendar
246         $calendar=<<EOF;
247 <table class="year-calendar">
248         <caption class="year-calendar-head">
249         $purl
250         $url
251         $nurl
252         </caption>
253         <tr>
254                 <th class="year-calendar-subhead" colspan="$params{months_per_row}">Months</th>
255         </tr>
256 EOF
257
258         for ($month = 1; $month <= 12; $month++) {
259                 my @day=localtime(timelocal(0,0,0,15,$month-1,$year-1900));
260                 my $murl;
261                 my $monthname = POSIX::strftime("%B", @day);
262                 my $monthabbr = POSIX::strftime("%b", @day);
263                 $calendar.=qq{\t<tr>\n}  if ($month % $params{months_per_row} == 1);
264                 my $tag;
265                 my $mtag=sprintf("%02d", $month);
266                 if ($month == $params{month}) {
267                         if ($cache{$pagespec}{"$year/$mtag"}) {
268                                 $tag = 'this_month_link';
269                         }
270                         else {
271                                 $tag = 'this_month_nolink';
272                         }
273                 }
274                 elsif ($cache{$pagespec}{"$year/$mtag"}) {
275                         $tag = 'month_link';
276                 } 
277                 elsif ($future_month && $month >= $future_month) {
278                         $tag = 'month_future';
279                 } 
280                 else {
281                         $tag = 'month_nolink';
282                 }
283
284                 if ($cache{$pagespec}{"$year/$mtag"}) {
285                         $murl = htmllink($params{page}, $params{destpage}, 
286                                 "$archivebase/$year/$mtag",
287                                 linktext => "$monthabbr");
288                         $calendar.=qq{\t<td class="$tag">};
289                         $calendar.=$murl;
290                         $calendar.=qq{\t</td>\n};
291                 }
292                 else {
293                         $calendar.=qq{\t<td class="$tag">$monthabbr</td>\n};
294                 }
295                 add_depends($params{page}, "$archivebase/$year/$mtag");
296
297                 $calendar.=qq{\t</tr>\n} if ($month % $params{months_per_row} == 0);
298         }
299
300         $calendar.=<<EOF;
301 </table>
302 EOF
303
304         return $calendar;
305 } #}}}
306
307 sub preprocess (@) { #{{{
308         my %params=@_;
309         $params{pages} = "*"            unless defined $params{pages};
310         $params{type}  = "month"        unless defined $params{type};
311         $params{month} = sprintf("%02d", $params{month}) if defined  $params{month};
312         $params{week_start_day} = 0     unless defined $params{week_start_day};
313         $params{months_per_row} = 3     unless defined $params{months_per_row};
314
315         if (! defined $params{year} || ! defined $params{month}) {
316                 # Record that the calendar next changes at midnight.
317                 $pagestate{$params{destpage}}{calendar}{nextchange}=($time
318                         + (60 - $now[0])                # seconds
319                         + (59 - $now[1]) * 60           # minutes
320                         + (23 - $now[2]) * 60 * 60      # hours
321                 );
322                 
323                 $params{year}  = 1900 + $now[5] unless defined $params{year};
324                 $params{month} = 1    + $now[4] unless defined $params{month};
325         }
326         else {
327                 delete $pagestate{$params{destpage}}{calendar};
328         }
329
330         # Calculate month names for next month, and previous months
331         my $pmonth = $params{month} - 1;
332         my $nmonth = $params{month} + 1;
333         my $pyear  = $params{year}  - 1;
334         my $nyear  = $params{year}  + 1;
335
336         # Adjust for January and December
337         if ($params{month} == 1) {
338                 $pmonth = 12;
339                 $pyear--;
340         }
341         if ($params{month} == 12) {
342                 $nmonth = 1;
343                 $nyear++;
344         }
345
346         $params{pmonth}=$pmonth;
347         $params{nmonth}=$nmonth;
348         $params{pyear} =$pyear;
349         $params{nyear} =$nyear;
350
351         my $calendar="\n";
352         my $pagespec=$params{pages};
353         my $page =$params{page};
354
355         if (! defined $cache{$pagespec}) {
356                 foreach my $p (keys %pagesources) {
357                         next unless pagespec_match($p, $pagespec);
358                         my $mtime = $IkiWiki::pagectime{$p};
359                         my $src   = $pagesources{$p};
360                         my @date  = localtime($mtime);
361                         my $mday  = $date[3];
362                         my $month = $date[4] + 1;
363                         my $year  = $date[5] + 1900;
364                         my $mtag  = sprintf("%02d", $month);
365
366                         # Only one posting per day is being linked to.
367                         $linkcache{"$year/$mtag/$mday"} = "$src";
368                         $cache{$pagespec}{"$year"}++;
369                         $cache{$pagespec}{"$year/$mtag"}++;
370                         $cache{$pagespec}{"$year/$mtag/$mday"}++;
371                 }
372         }
373
374         if ($params{type} =~ /month/i) {
375                 $calendar=format_month(%params);
376         }
377         elsif ($params{type} =~ /year/i) {
378                 $calendar=format_year(%params);
379         }
380
381         return "\n<div><div class=\"calendar\">$calendar</div></div>\n";
382 } #}}
383
384 sub needsbuild (@) { #{{{
385         my $needsbuild=shift;
386         foreach my $page (keys %pagestate) {
387                 if (exists $pagestate{$page}{calendar}{nextchange}) {
388                         if ($pagestate{$page}{calendar}{nextchange} <= $time) {
389                                 # force a rebuild so the calendar shows
390                                 # the current day
391                                 push @$needsbuild, $pagesources{$page};
392                         }
393                         if (exists $pagesources{$page} && 
394                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
395                                 # remove state, will be re-added if
396                                 # the calendar is still there during the
397                                 # rebuild
398                                 delete $pagestate{$page}{calendar};
399                         }
400                 }
401         }
402 } # }}}
403
404 1