]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/calendar.pm
Simplifying code
[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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require 5.002;
19 package IkiWiki::Plugin::calendar;
20
21 use warnings;
22 use strict;
23 use IkiWiki 3.00;
24 use Time::Local;
25
26 my $time=time;
27 my @now=localtime($time);
28
29 sub import {
30         hook(type => "checkconfig", id => "calendar", call => \&checkconfig);
31         hook(type => "getsetup", id => "calendar", call => \&getsetup);
32         hook(type => "needsbuild", id => "calendar", call => \&needsbuild);
33         hook(type => "preprocess", id => "calendar", call => \&preprocess);
34         hook(type => "scan", id => "calendar", call => \&scan);
35 }
36
37 sub getsetup () {
38         return
39                 plugin => {
40                         safe => 1,
41                         rebuild => undef,
42                         section => "widget",
43                 },
44                 archivebase => {
45                         type => "string",
46                         example => "archives",
47                         description => "base of the archives hierarchy",
48                         safe => 1,
49                         rebuild => 1,
50                 },
51                 archive_pagespec => {
52                         type => "pagespec",
53                         example => "page(posts/*) and !*/Discussion",
54                         description => "PageSpec of pages to include in the archives, if option `calendar_autocreate` is true.",
55                         link => 'ikiwiki/PageSpec',
56                         safe => 1,
57                         rebuild => 0,
58                 },
59                 calendar_autocreate => {
60                         type => "boolean",
61                         example => 1,
62                         description => "autocreate new calendar pages?",
63                         safe => 1,
64                         rebuild => undef,
65                 },
66                 calendar_autocreate_commit => {
67                         type => "boolean",
68                         example => 1,
69                         default => 1,
70                         description => "commit autocreated calendar pages",
71                         safe => 1,
72                         rebuild => 0,
73                 },
74 }
75
76 sub checkconfig () {
77         if (! defined $config{calendar_autocreate}) {
78                 $config{calendar_autocreate} = defined $config{archivebase} || defined $config{calendar_autocreate_commit};
79         }
80         if (! defined $config{calendar_autocreate_commit}) {
81                 $config{calendar_autocreate_commit} = 1;
82         }
83         if (! defined $config{archive_pagespec}) {
84                 $config{archive_pagespec} = '*';
85         }
86         if (! defined $config{archivebase}) {
87                 $config{archivebase} = 'archives';
88         }
89 }
90
91 sub is_leap_year (@) {
92         my %params=@_;
93         return ($params{year} % 4 == 0 && (($params{year} % 100 != 0) || $params{year} % 400 == 0));
94 }
95
96 sub month_days {
97         my %params=@_;
98         my $days_in_month = (31,28,31,30,31,30,31,31,30,31,30,31)[$params{month}-1];
99         if ($params{month} == 2 && is_leap_year(%params)) {
100                 $days_in_month++;
101         }
102         return $days_in_month;
103 }
104
105 sub autocreate {
106         my ($page, $pagefile, $year, $month) = @_;
107         my $message=sprintf(gettext("creating calendar page %s"), $page);
108         debug($message);
109
110         my $template;
111         if (defined $month) {
112                 $template=template("calendarmonth.tmpl");
113         } else {
114                 $template=template("calendaryear.tmpl");
115         }
116         $template->param(year => $year);
117         $template->param(month => $month) if defined $month;
118         $template->param(pagespec => $config{archive_pagespec});
119
120         my $dir = $config{srcdir};
121         if (! $config{calendar_autocreate_commit}) {
122                 $dir = $IkiWiki::Plugin::transient::transientdir;
123         }
124
125         writefile($pagefile, $dir, $template->output);
126         if ($config{rcs} && $config{calendar_autocreate_commit}) {
127                 IkiWiki::disable_commit_hook();
128                 IkiWiki::rcs_add($pagefile);
129                 IkiWiki::rcs_commit_staged(message => $message);
130                 IkiWiki::enable_commit_hook();
131         }
132 }
133
134 sub calendarlink($;$) {
135         my ($year, $month) = @_;
136         if (defined $month) {
137                 return $config{archivebase} . "/" . $year . "/" . $month;
138         } else {
139                 return $config{archivebase} . "/" . $year;
140         }
141 }
142
143 sub gencalendaryear {
144         my $year = shift;
145         my %params = @_;
146
147         return unless $config{calendar_autocreate};
148
149         # Building year page
150         my $page = calendarlink($year);
151         my $pagefile = newpagefile($page, $config{default_pageext});
152         add_autofile(
153                 $pagefile, "calendar",
154                 sub {return autocreate($page, $pagefile, $year);}
155         );
156
157         # Building month pages
158         foreach my $month (qw{01 02 03 04 05 06 07 08 09 10 11 12}) {
159                 my $page = calendarlink($year, $month);
160                 my $pagefile = newpagefile($page, $config{default_pageext});
161                 add_autofile(
162                         $pagefile, "calendar",
163                         sub {return autocreate($page, $pagefile, $year, $month);}
164                 );
165         }
166
167         # Filling potential gaps in years (e.g. calendar goes from 2010 to 2014,
168         # and we just added year 2005. We have to had years 2006 to 2009).
169         return if $params{norecurse};
170         if (not exists $wikistate{calendar}{minyear}) {
171                 $wikistate{calendar}{minyear} = $year;
172         } elsif ($wikistate{calendar}{minyear} > $year) {
173                 foreach my $other ($year + 1 .. $wikistate{calendar}{minyear} - 1) {
174                         gencalendaryear($other, norecurse => 1);
175                 }
176                 $wikistate{calendar}{minyear} = $year;
177         }
178         if (not exists $wikistate{calendar}{maxyear}) {
179                 $wikistate{calendar}{maxyear} = $year;
180         } elsif ($wikistate{calendar}{maxyear} < $year) {
181                 foreach my $other ($wikistate{calendar}{maxyear} + 1 .. $year - 1) {
182                         gencalendaryear($other, norecurse => 1);
183                 }
184                 $wikistate{calendar}{maxyear} = $year;
185         }
186 }
187
188 sub format_month (@) {
189         my %params=@_;
190
191         my %linkcache;
192         foreach my $p (pagespec_match_list($params{page}, 
193                                 "creation_year($params{year}) and creation_month($params{month}) and ($params{pages})",
194                                 # add presence dependencies to update
195                                 # month calendar when pages are added/removed
196                                 deptype => deptype("presence"))) {
197                 my $mtime = $IkiWiki::pagectime{$p};
198                 my @date  = localtime($mtime);
199                 my $mday  = $date[3];
200                 my $month = $date[4] + 1;
201                 my $year  = $date[5] + 1900;
202                 my $mtag  = sprintf("%02d", $month);
203
204                 if (! $linkcache{"$year/$mtag/$mday"}) {
205                         $linkcache{"$year/$mtag/$mday"} = [];
206                 }
207                 push(@{$linkcache{"$year/$mtag/$mday"}}, $p);
208         }
209                 
210         my $pmonth = $params{month} - 1;
211         my $nmonth = $params{month} + 1;
212         my $pyear  = $params{year};
213         my $nyear  = $params{year};
214
215         # Adjust for January and December
216         if ($params{month} == 1) {
217                 $pmonth = 12;
218                 $pyear--;
219         }
220         if ($params{month} == 12) {
221                 $nmonth = 1;
222                 $nyear++;
223         }
224
225         # Add padding.
226         $pmonth=sprintf("%02d", $pmonth);
227         $nmonth=sprintf("%02d", $nmonth);
228
229         my $calendar="\n";
230
231         # When did this month start?
232         my @monthstart = localtime(timelocal(0,0,0,1,$params{month}-1,$params{year}-1900));
233
234         my $future_dom = 0;
235         my $today      = 0;
236         if ($params{year} == $now[5]+1900 && $params{month} == $now[4]+1) {
237                 $future_dom = $now[3]+1;
238                 $today      = $now[3];
239         }
240
241         # Find out month names for this, next, and previous months
242         my $monthabbrev=strftime_utf8("%b", @monthstart);
243         my $monthname=strftime_utf8("%B", @monthstart);
244         my $pmonthname=strftime_utf8("%B", localtime(timelocal(0,0,0,1,$pmonth-1,$pyear-1900)));
245         my $nmonthname=strftime_utf8("%B", localtime(timelocal(0,0,0,1,$nmonth-1,$nyear-1900)));
246
247         my $archivebase = 'archives';
248         $archivebase = $config{archivebase} if defined $config{archivebase};
249         $archivebase = $params{archivebase} if defined $params{archivebase};
250   
251         # Calculate URL's for monthly archives.
252         my ($url, $purl, $nurl)=("$monthname $params{year}",'','');
253         if (exists $pagesources{"$archivebase/$params{year}/$params{month}"}) {
254                 $url = htmllink($params{page}, $params{destpage}, 
255                         "$archivebase/$params{year}/".$params{month},
256                         noimageinline => 1,
257                         linktext => "$monthabbrev $params{year}",
258                         title => $monthname);
259         }
260         add_depends($params{page}, "$archivebase/$params{year}/$params{month}",
261                 deptype("presence"));
262         if (exists $pagesources{"$archivebase/$pyear/$pmonth"}) {
263                 $purl = htmllink($params{page}, $params{destpage}, 
264                         "$archivebase/$pyear/$pmonth",
265                         noimageinline => 1,
266                         linktext => "\&larr;",
267                         title => $pmonthname);
268         }
269         add_depends($params{page}, "$archivebase/$pyear/$pmonth",
270                 deptype("presence"));
271         if (exists $pagesources{"$archivebase/$nyear/$nmonth"}) {
272                 $nurl = htmllink($params{page}, $params{destpage}, 
273                         "$archivebase/$nyear/$nmonth",
274                         noimageinline => 1,
275                         linktext => "\&rarr;",
276                         title => $nmonthname);
277         }
278         add_depends($params{page}, "$archivebase/$nyear/$nmonth",
279                 deptype("presence"));
280
281         # Start producing the month calendar
282         $calendar=<<EOF;
283 <table class="month-calendar">
284         <tr>
285         <th class="month-calendar-arrow">$purl</th>
286         <th class="month-calendar-head" colspan="5">$url</th>
287         <th class="month-calendar-arrow">$nurl</th>
288         </tr>
289         <tr>
290 EOF
291
292         # Suppose we want to start the week with day $week_start_day
293         # If $monthstart[6] == 1
294         my $week_start_day = $params{week_start_day};
295
296         my $start_day = 1 + (7 - $monthstart[6] + $week_start_day) % 7;
297         my %downame;
298         my %dowabbr;
299         for my $dow ($week_start_day..$week_start_day+6) {
300                 my @day=localtime(timelocal(0,0,0,$start_day++,$params{month}-1,$params{year}-1900));
301                 my $downame = strftime_utf8("%A", @day);
302                 my $dowabbr = substr($downame, 0, 1);
303                 $downame{$dow % 7}=$downame;
304                 $dowabbr{$dow % 7}=$dowabbr;
305                 $calendar.= qq{\t\t<th class="month-calendar-day-head $downame" title="$downame">$dowabbr</th>\n};
306         }
307
308         $calendar.=<<EOF;
309         </tr>
310 EOF
311
312         my $wday;
313         # we start with a week_start_day, and skip until we get to the first
314         for ($wday=$week_start_day; $wday != $monthstart[6]; $wday++, $wday %= 7) {
315                 $calendar.=qq{\t<tr>\n} if $wday == $week_start_day;
316                 $calendar.=qq{\t\t<td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
317         }
318
319         # At this point, either the first is a week_start_day, in which case
320         # nothing has been printed, or else we are in the middle of a row.
321         for (my $day = 1; $day <= month_days(year => $params{year}, month => $params{month});
322              $day++, $wday++, $wday %= 7) {
323                 # At this point, on a week_start_day, we close out a row,
324                 # and start a new one -- unless it is week_start_day on the
325                 # first, where we do not close a row -- since none was started.
326                 if ($wday == $week_start_day) {
327                         $calendar.=qq{\t</tr>\n} unless $day == 1;
328                         $calendar.=qq{\t<tr>\n};
329                 }
330                 
331                 my $tag;
332                 my $key="$params{year}/$params{month}/$day";
333                 if (defined $linkcache{$key}) {
334                         if ($day == $today) {
335                                 $tag='month-calendar-day-this-day';
336                         }
337                         else {
338                                 $tag='month-calendar-day-link';
339                         }
340                         $calendar.=qq{\t\t<td class="$tag $downame{$wday}">};
341                         $calendar.=qq{<div class='popup'>$day<div class='balloon'>};
342                         # Several postings on this page
343                         $calendar.=qq{<ul>};
344                         foreach my $page (@{$linkcache{$key}}) {
345                                 $calendar.= qq{\n\t\t\t<li>};
346                                 my $title;
347                                 if (exists $pagestate{$page}{meta}{title}) {
348                                         $title = "$pagestate{$page}{meta}{title}";
349                                 }
350                                 else {
351                                         $title = pagetitle(IkiWiki::basename($page));
352                                 }
353                                 $calendar.=htmllink($params{page}, $params{destpage}, 
354                                         $page,
355                                         noimageinline => 1,
356                                         linktext => $title,
357                                         title => $title);
358                                 $calendar.= '</li>';
359                         }
360                         $calendar.=qq{\n\t\t</ul>};
361                         $calendar.=qq{</div></div>};
362                         $calendar.=qq{</td>\n};
363                 }
364                 else {
365                         if ($day == $today) {
366                                 $tag='month-calendar-day-this-day';
367                         }
368                         elsif ($day == $future_dom) {
369                                 $tag='month-calendar-day-future';
370                         }
371                         else {
372                                 $tag='month-calendar-day-nolink';
373                         }
374                         $calendar.=qq{\t\t<td class="$tag $downame{$wday}">$day</td>\n};
375                 }
376         }
377
378         # finish off the week
379         for (; $wday != $week_start_day; $wday++, $wday %= 7) {
380                 $calendar.=qq{\t\t<td class="month-calendar-day-noday $downame{$wday}">&nbsp;</td>\n};
381         }
382         $calendar.=<<EOF;
383         </tr>
384 </table>
385 EOF
386
387         return $calendar;
388 }
389
390 sub format_year (@) {
391         my %params=@_;
392
393         my @post_months;
394         foreach my $p (pagespec_match_list($params{page}, 
395                                 "creation_year($params{year}) and ($params{pages})",
396                                 # add presence dependencies to update
397                                 # year calendar's links to months when
398                                 # pages are added/removed
399                                 deptype => deptype("presence"))) {
400                 my $mtime = $IkiWiki::pagectime{$p};
401                 my @date  = localtime($mtime);
402                 my $month = $date[4] + 1;
403
404                 $post_months[$month]++;
405         }
406                 
407         my $calendar="\n";
408         
409         my $pyear = $params{year}  - 1;
410         my $nyear = $params{year}  + 1;
411
412         my $thisyear = $now[5]+1900;
413         my $future_month = 0;
414         $future_month = $now[4]+1 if $params{year} == $thisyear;
415
416         my $archivebase = 'archives';
417         $archivebase = $config{archivebase} if defined $config{archivebase};
418         $archivebase = $params{archivebase} if defined $params{archivebase};
419
420         # calculate URL's for previous and next years
421         my ($url, $purl, $nurl)=("$params{year}",'','');
422         if (exists $pagesources{"$archivebase/$params{year}"}) {
423                 $url = htmllink($params{page}, $params{destpage}, 
424                         "$archivebase/$params{year}",
425                         noimageinline => 1,
426                         linktext => $params{year},
427                         title => $params{year});
428         }
429         add_depends($params{page}, "$archivebase/$params{year}", deptype("presence"));
430         if (exists $pagesources{"$archivebase/$pyear"}) {
431                 $purl = htmllink($params{page}, $params{destpage}, 
432                         "$archivebase/$pyear",
433                         noimageinline => 1,
434                         linktext => "\&larr;",
435                         title => $pyear);
436         }
437         add_depends($params{page}, "$archivebase/$pyear", deptype("presence"));
438         if (exists $pagesources{"$archivebase/$nyear"}) {
439                 $nurl = htmllink($params{page}, $params{destpage}, 
440                         "$archivebase/$nyear",
441                         noimageinline => 1,
442                         linktext => "\&rarr;",
443                         title => $nyear);
444         }
445         add_depends($params{page}, "$archivebase/$nyear", deptype("presence"));
446
447         # Start producing the year calendar
448         my $m=$params{months_per_row}-2;
449         $calendar=<<EOF;
450 <table class="year-calendar">
451         <tr>
452         <th class="year-calendar-arrow">$purl</th>
453         <th class="year-calendar-head" colspan="$m">$url</th>
454         <th class="year-calendar-arrow">$nurl</th>
455         </tr>
456         <tr>
457                 <th class="year-calendar-subhead" colspan="$params{months_per_row}">Months</th>
458         </tr>
459 EOF
460
461         for (my $month = 1; $month <= 12; $month++) {
462                 my @day=localtime(timelocal(0,0,0,15,$month-1,$params{year}-1900));
463                 my $murl;
464                 my $monthname = strftime_utf8("%B", @day);
465                 my $monthabbr = strftime_utf8("%b", @day);
466                 $calendar.=qq{\t<tr>\n}  if ($month % $params{months_per_row} == 1);
467                 my $tag;
468                 my $mtag=sprintf("%02d", $month);
469                 if ($month == $params{month} && $thisyear == $params{year}) {
470                         $tag = 'year-calendar-this-month';
471                 }
472                 elsif ($pagesources{"$archivebase/$params{year}/$mtag"}) {
473                         $tag = 'year-calendar-month-link';
474                 } 
475                 elsif ($future_month && $month >= $future_month) {
476                         $tag = 'year-calendar-month-future';
477                 } 
478                 else {
479                         $tag = 'year-calendar-month-nolink';
480                 }
481
482                 if ($pagesources{"$archivebase/$params{year}/$mtag"} &&
483                     $post_months[$mtag]) {
484                         $murl = htmllink($params{page}, $params{destpage}, 
485                                 "$archivebase/$params{year}/$mtag",
486                                 noimageinline => 1,
487                                 linktext => $monthabbr,
488                                 title => $monthname);
489                         $calendar.=qq{\t<td class="$tag">};
490                         $calendar.=$murl;
491                         $calendar.=qq{\t</td>\n};
492                 }
493                 else {
494                         $calendar.=qq{\t<td class="$tag">$monthabbr</td>\n};
495                 }
496                 add_depends($params{page}, "$archivebase/$params{year}/$mtag",
497                         deptype("presence"));
498
499                 $calendar.=qq{\t</tr>\n} if ($month % $params{months_per_row} == 0);
500         }
501
502         $calendar.=<<EOF;
503 </table>
504 EOF
505
506         return $calendar;
507 }
508
509 sub setnextchange ($$) {
510         my $page=shift;
511         my $timestamp=shift;
512
513         if (! exists $pagestate{$page}{calendar}{nextchange} ||
514             $pagestate{$page}{calendar}{nextchange} > $timestamp) {
515                 $pagestate{$page}{calendar}{nextchange}=$timestamp;
516         }
517 }
518
519 sub preprocess (@) {
520         my %params=@_;
521
522         my $thisyear=1900 + $now[5];
523         my $thismonth=1 + $now[4];
524
525         $params{pages} = "*"            unless defined $params{pages};
526         $params{type}  = "month"        unless defined $params{type};
527         $params{week_start_day} = 0     unless defined $params{week_start_day};
528         $params{months_per_row} = 3     unless defined $params{months_per_row};
529         $params{year}  = $thisyear      unless defined $params{year};
530         $params{month} = $thismonth     unless defined $params{month};
531
532         my $relativeyear=0;
533         if ($params{year} < 1) {
534                 $relativeyear=1;
535                 $params{year}=$thisyear+$params{year};
536         }
537         my $relativemonth=0;
538         if ($params{month} < 1) {
539                 $relativemonth=1;
540                 my $monthoff=$params{month};
541                 $params{month}=($thismonth+$monthoff) % 12;
542                 $params{month}=12 if $params{month}==0;
543                 my $yearoff=POSIX::ceil(($thismonth-$params{month}) / -12)
544                         - int($monthoff / 12);
545                 $params{year}-=$yearoff;
546         }
547         
548         $params{month} = sprintf("%02d", $params{month});
549         
550         if ($params{type} eq 'month' && $params{year} == $thisyear
551             && $params{month} == $thismonth) {
552                 # calendar for current month, updates next midnight
553                 setnextchange($params{destpage}, ($time
554                         + (60 - $now[0])                # seconds
555                         + (59 - $now[1]) * 60           # minutes
556                         + (23 - $now[2]) * 60 * 60      # hours
557                 ));
558         }
559         elsif ($params{type} eq 'month' &&
560                (($params{year} == $thisyear && $params{month} > $thismonth) ||
561                 $params{year} > $thisyear)) {
562                 # calendar for upcoming month, updates 1st of that month
563                 setnextchange($params{destpage},
564                         timelocal(0, 0, 0, 1, $params{month}-1, $params{year}));
565         }
566         elsif (($params{type} eq 'year' && $params{year} == $thisyear) ||
567                $relativemonth) {
568                 # Calendar for current year updates 1st of next month.
569                 # Any calendar relative to the current month also updates
570                 # then.
571                 if ($thismonth < 12) {
572                         setnextchange($params{destpage},
573                                 timelocal(0, 0, 0, 1, $thismonth+1-1, $params{year}));
574                 }
575                 else {
576                         setnextchange($params{destpage},
577                                 timelocal(0, 0, 0, 1, 1-1, $params{year}+1));
578                 }
579         }
580         elsif ($relativeyear) {
581                 # Any calendar relative to the current year updates 1st
582                 # of next year.
583                 setnextchange($params{destpage},
584                         timelocal(0, 0, 0, 1, 1-1, $thisyear+1));
585         }
586         elsif ($params{type} eq 'year' && $params{year} > $thisyear) {
587                 # calendar for upcoming year, updates 1st of that year
588                 setnextchange($params{destpage},
589                         timelocal(0, 0, 0, 1, 1-1, $params{year}));
590         }
591         else {
592                 # calendar for past month or year, does not need
593                 # to update any more
594                 delete $pagestate{$params{destpage}}{calendar};
595         }
596
597         my $calendar="";
598         if ($params{type} eq 'month') {
599                 $calendar=format_month(%params);
600         }
601         elsif ($params{type} eq 'year') {
602                 $calendar=format_year(%params);
603         }
604
605         return "\n<div><div class=\"calendar\">$calendar</div></div>\n";
606 } #}}
607
608 sub needsbuild (@) {
609         my $needsbuild=shift;
610         foreach my $page (keys %pagestate) {
611                 if (exists $pagestate{$page}{calendar}{nextchange}) {
612                         if ($pagestate{$page}{calendar}{nextchange} <= $time) {
613                                 # force a rebuild so the calendar shows
614                                 # the current day
615                                 push @$needsbuild, $pagesources{$page};
616                         }
617                         if (exists $pagesources{$page} && 
618                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
619                                 # remove state, will be re-added if
620                                 # the calendar is still there during the
621                                 # rebuild
622                                 delete $pagestate{$page}{calendar};
623                         }
624                 }
625         }
626
627         return $needsbuild;
628 }
629
630 sub scan (@) {
631         my %params=@_;
632         my $page=$params{page};
633
634         # Check if year pages have to be generated
635         if (pagespec_match($page, $config{archive_pagespec})) {
636                 my @ctime = localtime($IkiWiki::pagectime{$page});
637                 gencalendaryear($ctime[5]+1900);
638         }
639 }
640
641 1