1 # Blosxom Plugin: calendar -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
4 # Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
5 # Calendar plugin Home/Docs/Licensing:
6 # http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Calendar/
10 # --- Configuration Variables ---
12 @monthname = qw/January February March
15 October November December/ if ($#monthname != 11);
16 @monthabbr = qw/Jan Feb Mar
19 Oct Nov Dec/ if ($#monthabbr != 11);
20 @downame = qw/Sunday Monday Tuesday Wednesday Thursday
21 Friday Saturday/ if ($#downame != 6);
22 @dowabbr = qw/Sun Mon Tue Wed Thu Fri Sat/ if ($#dowabbr != 6);
25 if not defined $first_dow;
27 # set to 0 to disable attempted caching
28 $use_caching = 1 unless defined $use_caching;
29 $months_per_row = 3 unless defined $months_per_row;
30 $debug_level = 1 unless defined $debug_level;
31 # -------------------------------------------------------------------
38 $prev_month_link = '';
39 $next_month_link = '';
43 my $package = "calendar";
44 my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
50 my ($level, @msg) = @_;
52 if ($debug_level >= $level) {
53 print STDERR "$package debug $level: @msg\n";
60 return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
64 my ($bit, $year, $month, $day, $dow) = @_;
65 my ($monthname, $monthabbr) = ($monthname[$month-1], $monthabbr[$month-1]);
66 my ($downame, $dowabbr) = ($downame[$dow], $dowabbr[$dow]);
67 my $year2digit = sprintf("%02d", $year % 100);
69 my $url = $blosxom::url;
70 $url .= sprintf("/%04d/", $year) if defined $year;
71 $url .= sprintf("%02d/", $month) if defined $month;
72 $url .= sprintf("%02d/", $day) if defined $day;
75 $date .= "$year" if defined $year;
76 $date .= "/$month" if defined $month;
77 $date .= "/$day" if defined $day;
78 my $count = $cache->{stories}{$date};
80 my $f = load_template($bit);
81 $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
86 my ($year, $month) = @_;
87 my $days = (31,28,31,30,31,30,31,31,30,31,30,31)[$month-1];
90 (($year % 100 != 0) ||
91 ($year % 400 == 0)))) {
98 my ($year, $month, $day);
100 if (defined($blosxom::path_info_yr)) {
101 $year = $blosxom::path_info_yr + 0;
102 $month = $blosxom::path_info_mo_num + 0;
103 $day = $blosxom::path_info_da + 0;
104 if ($month == 0 && $cache->{stories}{$year}) {
105 for ($month = 12; $month > 0; $month--) {
106 last if $cache->{stories}{"$year/$month"};
112 # is this a single-article view?
113 # XXX this probably doesn't work for static
114 if ($blosxom::path_info =~ m!\.!) {
115 my $filename = $blosxom::path_info;
116 # replace whatever flavour with the file extension
117 $filename =~ s!\..*!.$blosxom::file_extension!;
118 # remove any dated dirs, if present
119 $filename =~ s!/\d+/.*/!/!;
120 # and convert from an URL relative to a filename
121 $filename = "$blosxom::datadir/$filename";
122 # at this point, it's our best guess at a filename
123 # if it's not in the index, oh well, we tried - fall back to present
124 $now = $files->{$filename} || $^T;
128 # no date given at all, do this month and highlight today
129 my @now = localtime($now);
130 $year = $now[5] + 1900;
131 $month = $now[4] + 1;
134 return ($year, $month, $day);
137 sub build_prev_month_link {
138 my ($year, $month) = @_;
139 my $year_orig = $year;
140 my $month_orig = $month;
144 if ($month <= 0) { # == 0 is right, <= 2xprotects against infinite loop bug
147 # XXX assumption: once a log is active, no full years are skipped
148 if ($cache->{stories}{"$year"} == 0) {
149 return report('prev_month_nolink',
150 $month_orig == 1 ? $year_orig-1 : $year_orig,
151 $month_orig == 1 ? 12 : $month_orig-1);
154 if ($cache->{stories}{"$year/$month"}) {
155 return report('prev_month_link', $year, $month);
160 sub build_next_month_link {
161 my ($year, $month) = @_;
162 my $year_orig = $year;
163 my $month_orig = $month;
170 # XXX assumption: once a log is active, no full years are skipped
171 if ($cache->{stories}{"$year"} == 0) {
172 return report('next_month_nolink',
173 $month_orig == 1 ? $year_orig-1 : $year_orig,
174 $month_orig == 1 ? 12 : $month_orig-1);
177 if ($cache->{stories}{"$year/$month"}) {
178 return report('next_month_link', $year, $month);
183 sub build_prev_year_link {
187 return report($cache->{stories}{"$year"} ?
188 'prev_year_link': 'prev_year_nolink',
192 sub build_next_year_link {
197 return report($cache->{stories}{"$year"} ?
198 'next_year_link': 'next_year_nolink',
202 sub build_month_calendar {
203 my ($year, $month, $highlight_dom) = @_;
206 my (@now, $monthstart, @monthstart);
207 my ($day, $days, $wday);
210 @now = localtime($^T);
211 $future_dom = $now[3]+1 if ($year == $now[5]+1900 && $month == $now[4]+1);
213 $days = days_in_month($year, $month);
214 $monthstart = timelocal(0,0,0,1,$month-1,$year-1900);
215 @monthstart = localtime($monthstart);
217 $results = report('month_head', $year, $month);
218 $results .= report('month_sub_head', $year, $month);
221 $results .= report('month_sub_day', $year, $month, undef, $wday);
224 } while ($wday != $first_dow);
225 $results .= report('month_sub_foot', $year, $month);
227 # First, skip over the first partial week (possibly empty)
228 # before the month started
229 for ($wday = $first_dow; $wday != $monthstart[6]; $wday++, $wday %= 7) {
230 $results .= report('week_head', $year, $month)
231 if ($wday == $first_dow);
232 $results .= report('noday', $year, $month, undef, $wday);
235 # now do the month itself
236 for ($day = 1; $day <= $days; $day++) {
237 $results .= report('week_head', $year, $month, $day)
238 if ($wday == $first_dow);
240 if ($day == $highlight_dom) {
241 if ($cache->{stories}{"$year/$month/$day"}){$tag='this_day_link'}
242 else {$tag = 'this_day_nolink'}}
243 elsif ($cache->{stories}{"$year/$month/$day"}){$tag = 'day_link'}
244 elsif ($future_dom && $day >= $future_dom) {$tag = 'day_future'}
245 else {$tag = 'day_nolink'}
246 $results .= report($tag, $year, $month, $day, $wday);
247 $wday = 0 if (++$wday == 7);
248 $results .= report('week_foot', $year, $month)
249 if ($wday == $first_dow);
252 # and finish up the last week, if any left
253 if ($wday != $first_dow) {
254 for(; $wday != $first_dow; $wday++, $wday %= 7) {
255 $results .= report('noday', $year, $month, undef, $wday);
257 $results .= report('week_foot', $year, $month);
260 $results .= report('month_foot', $year, $month);
265 sub build_year_calendar {
266 my ($year, $highlight_month) = @_;
269 my $future_month = 0;
271 @now = localtime($^T);
272 $future_month = $now[4]+1 if ($year == $now[5]+1900);
274 $results = report('year_head', $year);
275 for ($month = 1; $month <= 12; $month++) {
276 $results .= report('quarter_head', $year)
277 if ($month % $months_per_row == 1);
279 if ($month == $highlight_month) {
280 if ($cache->{stories}{"$year/$month"}) {$tag = 'this_month_link'}
281 else {$tag = 'this_month_nolink'}}
282 elsif ($cache->{stories}{"$year/$month"}) {$tag = 'month_link'}
283 elsif ($future_month && $month >=$future_month){$tag = 'month_future'}
284 else {$tag = 'month_nolink'}
285 $results .= report($tag, $year, $month);
286 $results .= report('quarter_foot', $year)
287 if ($month % $months_per_row == 0);
289 $results .= report('year_foot', $year);
295 return report('calendar');
300 my ($sub, $cachetag, @args) = @_;
301 my $cachekey = join '/',@args,$blosxom::flavour;
302 return $cache->{$cachetag}{$cachekey} ||=
303 (debug(1, "cache miss $cachetag @args $blosxom::flavour") and
304 ($save_cache = 1) and
309 my ($num_files) = @_;
310 return 0 if !$use_caching;
311 eval "require Storable";
313 debug(1, "cache disabled, Storable not available");
317 if (!Storable->can('lock_retrieve')) {
318 debug(1, "cache disabled, Storable::lock_retrieve not available");
322 $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
323 # >= rather than ==, so that if we're being used along with a search
324 # plugin that reduces %files, rather than dumping the cache and showing
325 # a limited calendar, we'll display the full thing (if available) . I
326 # think that's preferable as well as being more efficient.
327 # XXX improvement: rather than dumping the whole thing, just update the
328 # count and dump the current month (and sometimes the previous month
333 $today = "$now[5]/$now[4]/$now[3]";
335 $cache->{num_files} >= $num_files &&
336 $cache->{today} eq $today) {
337 debug(1, "Using cached state");
340 $cache = {num_files => $num_files, today => $today};
345 return if (!$use_caching || !$save_cache);
346 debug(1, "Saving cache");
347 -d $blosxom::plugin_state_dir
348 or mkdir $blosxom::plugin_state_dir;
349 Storable::lock_store($cache, $cachefile);
353 debug(1, "start() called, enabled");
357 last if /^(__END__)?$/;
358 my ($flavour, $comp, $txt) = split ' ',$_,3;
360 $blosxom::template{$flavour}{"$package.$comp"} = $txt;
366 my ($pkg, $files_ref) = @_;
367 debug(1, "filter() called");
371 my $num_files = scalar keys %$files;
372 my @latest = (sort {$b <=> $a} values %$files);
373 while ($latest[0] == $^T) {
377 return 1 if prime_cache($num_files);
379 debug(1, "cache miss: %stories");
381 foreach (keys %{$files}) {
383 my @date = localtime($files->{$_});
385 my $month = $date[4] + 1;
386 my $year = $date[5] + 1900;
387 $cache->{stories}{"$year"}++;
388 $cache->{stories}{"$year/$month"}++;
389 $cache->{stories}{"$year/$month/$mday"}++;
391 debug(1, "filter() done");
396 debug(1, "head() called");
397 my ($year, $month, $day) = pseudo_now();
399 debug(0,"Bad year $year requested ($year, path_info $ENV{PATH_INFO}, year to 2000");
402 $prev_month_link = cached(\&build_prev_month_link, "pml", $year,$month);
403 $next_month_link = cached(\&build_next_month_link, "nml", $year,$month);
404 $prev_year_link = cached(\&build_prev_year_link, "pyl", $year);
405 $next_year_link = cached(\&build_next_year_link, "nyl", $year);
406 $month_calendar = cached(\&build_month_calendar, "mc", $year,$month,$day);
408 for (my $y = (localtime)[5]+1900; $cache->{stories}{$y}; $y--) {
409 my $varname = "year_calendar_$y";
411 $year_calendar = cached(\&build_year_calendar, "yc",$year,$month);
412 $$varname = $year_calendar;
414 $$varname = cached(\&build_year_calendar, "yc", $y);
417 $year_calendar = cached(\&build_year_calendar, "yc", $year,$month)
418 unless $year_calendar;
419 $calendar = cached(\&build_calendar, "c", $year,$month,$day);
423 debug(1, "head() done, length(\$month_calendar, \$year_calendar, \$calendar) = ", length($month_calendar), length($year_calendar), length($calendar));
427 # these look better (to me), but don't use <caption> like they 'should', and
428 # the year one assumes 3 columns
429 #html month_head <table class="month-calendar"><tr class="month-calendar-head"><th align="left">$prev_month_link</th><th colspan="5"><a title="$monthname $year" href="$url">$monthname</a></th><th align="right">$next_month_link</th></tr>\n
430 #html year_head <table class="year-calendar"><tr class="year-calendar-head"><th align="left">$prev_year_link</th><th><a title="$year" href="$url">$year</a></th><th align="right">$next_year_link</th></tr><tr><th class="year-calendar-subhead" colspan=$months_per_row>Months</th></tr>\n
434 error month_head <table class="month-calendar"><caption class="month-calendar-head">$prev_month_link<a title="$monthname $year ($count)" href="$url">$monthname</a>$next_month_link</caption>\n
435 error month_sub_head <tr>\n
436 error month_sub_day <th class="month-calendar-day-head $downame">$dowabbr</th>\n
437 error month_sub_foot </tr>\n
438 error week_head <tr>\n
439 error noday <td class="month-calendar-day-noday $downame"> </td>\n
440 error day_link <td class="month-calendar-day-link $downame"><a title="$downame, $day $monthname $year ($count)" href="$url">$day</a></td>\n
441 error day_nolink <td class="month-calendar-day-nolink $downame">$day</td>\n
442 error day_future <td class="month-calendar-day-future $downame">$day</td>\n
443 error this_day_link <td class="month-calendar-day-this-day $downame"><a title="$downame, $day $monthname $year (current) ($count)" href="$url">$day</a></td>\n
444 error this_day_nolink <td class="month-calendar-day-this-day $downame">$day</td>\n
445 error week_foot </tr>\n
446 error month_foot </table>\n
447 error prev_month_link <a title="$monthname $year ($count)" href="$url">←</a>
448 error next_month_link <a title="$monthname $year ($count)" href="$url">→</a>
449 error prev_month_nolink ←
450 error next_month_nolink →
451 error year_head <table class="year-calendar"><caption class="year-calendar-head">$prev_year_link<a title="$year ($count)" href="$url">$year</a>$next_year_link</caption><tr><th class="year-calendar-subhead" colspan="$months_per_row">Months</th></tr>\n
452 error quarter_head <tr>\n
453 error month_link <td class="year-calendar-month-link"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>\n
454 error month_nolink <td class="year-calendar-month-nolink">$monthabbr</td>\n
455 error month_future <td class="year-calendar-month-future">$monthabbr</td>\n
456 error this_month_link <td class="year-calendar-this-month"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>
457 error this_month_nolink <td class="year-calendar-this-month">$monthabbr</td>
458 error quarter_foot </tr>\n
459 error year_foot </table>\n
460 error prev_year_link <a title="$year ($count)" href="$url">←</a>
461 error next_year_link <a title="$year ($count)" href="$url">→</a>
462 error prev_year_nolink ←
463 error next_year_nolink →
464 error calendar <div class="calendar"><table><tr><td>$calendar::month_calendar</td><td>$calendar::year_calendar</td></tr></table></div>
469 Blosxom Plug-in: calendar
473 Purpose: Provides a Radio-style archive navigation calendar
475 * $calendar::calendar -- side-by-side month and year calendars
476 * $calendar::month_calendar -- month calendar only
477 * $calendar::year_calendar -- year calendar only
478 * $calendar::year_calendar_2003 -- year calendar for 2003; these are built for every year with stories.
488 Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
492 None known; address bug reports and comments to me or to the Blosxom
493 mailing list [http://www.yahoogroups.com/groups.blosxom].
497 =head2 Configuration variables
499 C<@monthname>, C<@monthabbr>, C<@downame> and C<@dowabbr> contain the
500 long and short forms of the names of the months and days of the week;
501 @downame and @dowabbr should always start with Sunday, regardless of
504 C<$first_dow> sets the plugin's idea of the first day day of the week;
505 use 0 for Sunday, 1 for Monday, and so on; using a number outside the
506 range [0..6] will cause undefined behavior, possibly including a
509 C<$use_caching> controls whether or not to try to cache statistics and
510 formatted results; caching requires Storable, but the plugin will work
511 just fine without it.
513 C<$months_per_row> controls how many months are on each row of the
514 year calendar. This should be a number that evenly divides 12 (1, 2,
515 3, 4, 6 or 12), but nothing checks for that.
517 C<$debug_level> can be set to a value between 0 and 5; 0 will output
518 no debug information, while 5 will be very verbose. The default is 1,
519 and should be changed after you've verified the plugin is working
522 =head2 Classes for CSS control
524 There's an (over)abundance of classes used, available for CSS customization.
526 * C<calendar> -- the calendar as a whole
527 * C<month-calendar> -- the month calendar as a whole
528 * C<month-calendar-head> -- the head of the month calendar (ie,
530 * C<month-calendar-day-head> -- a column head in the month calendar
531 (ie, a day-of-week abbreviation)
532 * C<month-calendar-day-noday>, C<month-calendar-day-link>,
533 C<month-calendar-day-nolink>, C<month-calendar-day-future>,
534 C<month-calendar-day-this-day> -- the day squares on the month
535 calendar, for days that don't exist (before or after the month
536 itself), that don't have stories, that do have stories, that are
537 in the future, or are that currently selected, respectively
538 * Day-of-week-name -- each day square is also given a class matching
539 its day of week, from C<@downame>; this can be used to hilight
541 * C<year-calendar> -- the year calendar as a whole
542 * C<year-calendar-head> -- the head of the year calendar (ie,
544 * C<year-calendar-subhead> -- ie, "Months"
545 * C<year-calendar-month-link>, C<year-calendar-month-nolink>,
546 C<year-calendar-month-future>, C<year-calendar-this-month> -- the
547 month squares on the year calendar, for months with stories,
548 without, in the future, and currently selected, respectively.
550 =head2 Flavour-style files
552 If you want a format change that can't be made by CSS, you can
553 override the HTML generated by creating files similar to Blosxom's
554 flavour files. They should be named calendar.I<bit>.I<flavour>; for
555 available I<bit>s and their default meanings, see the C<__DATA__>
556 section in the plugin.
560 1. Download and unpack (if you're reading this, you've probably
562 2. Copy it to your plugins directory. Make sure it's world-readable.
563 3. Modify a C<head> or C<foot> file to include C<$calendar::calendar>,
564 C<$calendar::month_calendar> or C<$calendar::year_calendar>
565 4. Try it out -- load your blog in your browser. If you see a calendar, great!
566 5. Look at your error log. Verify you have an 'enabled' line.
567 6. If you're wanting to verify caching is working, load the page
568 again, and now look for an error log line "calendar debug 1: Using
570 7. Once you're satisfied it's working, edit the C<$debug_level> configuration
571 variable to C<0>. There are a couple other configuration variables you may
576 If the Storable module is available and $use_caching is set, various
577 bits of data will be cached; this includes the information on what
578 days have stories (and are thus linkable, and what months and years
579 are included in the next/forward lists), the contents of any flavour
580 files, and the final formatted output of any calendars generated.
582 The cache will be flushed whenever a story is added (but not when one
583 is removed), so in normal use should be invisible. If you're making
584 template changes however, or are removing stories, you may wish to
585 either disable the cache (by setting $use_caching to 0) or manually
586 flush the cache; this can be done by removing
587 $plugin_state_dir/.calendar.cache, and is always safe to do.
592 Copyright 2003, Todd Larason
594 (This license is the same as Blosxom's)
596 Permission is hereby granted, free of charge, to any person obtaining a
597 copy of this software and associated documentation files (the "Software"),
598 to deal in the Software without restriction, including without limitation
599 the rights to use, copy, modify, merge, publish, distribute, sublicense,
600 and/or sell copies of the Software, and to permit persons to whom the
601 Software is furnished to do so, subject to the following conditions:
603 The above copyright notice and this permission notice shall be included
604 in all copies or substantial portions of the Software.
606 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
607 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
608 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
609 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
610 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
611 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
612 OTHER DEALINGS IN THE SOFTWARE.