#!/usr/bin/env perl # SPDX-License-Identifier: GPL-2.0-only use strict; use warnings; use Getopt::Long; use JSON::MaybeXS; my %Options; my $rem2html_version = '2.1'; my($days, $shades, $moons, $classes, $Month, $Year, $Numdays, $Firstwkday, $Mondayfirst, $weeks, @Daynames, $Nextmon, $Nextlen, $Prevmon, $Prevlen); my $TIDY_PROGNAME = $0; $TIDY_PROGNAME =~ s|^.*/||; # rem2html -- convert the output of "remind -pp" to HTML =head1 NAME rem2html - Convert the output of "remind -pp" to HTML =head1 SYNOPSIS remind -pp ... | rem2html [options] You can also use the old interchange format as below, but the -pp version is preferred. remind -p ... | rem2html [options] =head1 OPTIONS =over 4 =item --help, -h Print usage information =item --version Print version =item --backurl I When producing the small calendar for the previous month, make the month name a link to I. =item --forwurl I When producing the small calendar for the next month, make the month name a link to I. =item --imgbase I When creating URLs for the stylesheet or external images, use I as the base URL. =item --pngs Normally, rem2html uses inline "data:" URLs for the moon phase images, yielding a standalone HTML file. The C<--pngs> option makes it use external images named firstquarter.png, fullmoon.png, lastquarter.png and newmoon.png, which are expected to live in C<--imgbase>. =item --stylesheet I Use I as the stylesheet. If this option is used, I is interpreted relative to B I it starts with a "/". =item --nostyle Produce basic HTML that does not use a CSS stylesheet. =item --tableonly Output results as a EtableE ... E/tableE sequence only without any EhtmlE or EbodyE tags. =item --title I Use I<title> as the content between E<lt>titleE<gt> and E<lt>/titleE<gt> tags. =item --prologue I<html_text> Insert I<html_text> right after the E<lt>bodyE<gt> tag. =item --epilogue I<html_text> Insert I<html_text> right before the E<lt>/bodyE<gt> tag. =back =head1 SPECIALS SUPPORTED The rem2html back-end supports the following SPECIAL reminders: =over =item HTML Add an HTML reminder to the calendar. All HTML tags are available. =item HTMLCLASS Add a CSS class to the box representing the trigger date. See "HIGHLIGHTING TODAY" for an example =item WEEK, MOON, SHARE, COLOR The standard SPECIALs supported by all back-ends =back =head1 HIGHLIGHTING TODAY Older versions of rem2html used to highlight today's date with a red outline. The current version does not do that by default. If you wish to highlight today's date, add the following reminder to your reminders file: REM [realtoday()] SPECIAL HTMLCLASS rem-today =head1 AUTHOR rem2html was written by Dianne Skoll with much inspiration from an earlier version by Don Schwarz. =head1 HOME PAGE L<https://dianne.skoll.ca/projects/remind/> =head1 SEE ALSO B<remind>, B<rem2ps>, B<rem2pdf>, B<tkremind> =cut sub usage { my ($exit_status) = @_; if (!defined($exit_status)) { $exit_status = 1; } print STDERR <<"EOM"; $TIDY_PROGNAME: Produce an HTML calendar from the output of "remind -pp" Usage: remind -pp ... | rem2html [options] Options: --help, -h Print usage information --man Show man page (requires "perldoc") --version Print version --backurl url Make the title on the previous month's small calendar entry a link to <url> --forwurl url Same as --backurl, but for the next month's small calendar --imgbase url Base URL of images and default stylesheet file --pngs Use external .PNG images for moon phases rater than inline data: URLs --stylesheet url.css URL of CSS stylesheet. If specified, imgbase is NOT prepended to url.css --nostyle Produce basic HTML that does not use a CSS stylesheet --tableonly Output results as a <table> only, no <html>, <body>, etc. --title string What to put in <title>... tags --prologue html_text Text to insert at the top of the body --epilogue html_text Text to insert at the end of the body EOM exit($exit_status); } sub smoosh { my ($first, $second) = @_; return $second unless defined ($first); return $second if $first eq ''; return $second if ($second =~ m|^/|); # Absolute path given for second # Squash multiple slashes $first =~ s|/+|/|g; # Special case return "/$second" if ($first eq '/'); # Delete trailing slash $first =~ s|/$||; return "$first/$second"; } sub parse_options { local $SIG{__WARN__} = sub { print STDERR "$TIDY_PROGNAME: $_[0]\n"; }; if (!GetOptions(\%Options, "help|h", "man", "pngs", "version", "stylesheet=s", "nostyle", "backurl=s", "forwurl=s", "title=s", "prologue=s", "epilogue=s", "imgbase=s", "tableonly")) { usage(1); } $Options{title} ||= 'HTML Calendar'; my $stylesheet = $Options{stylesheet}; if ($stylesheet) { $Options{stylesheet} = smoosh($Options{imgbase}, $stylesheet); } } sub start_output { return if ($Options{tableonly}); print("\n"); print("\n\n" . $Options{title} . "\n"); print('' . "\n"); print('' . "\n"); if (!$Options{nostyle}) { if ($Options{stylesheet}) { print('' . "\n"); } else { print("\n"); } } print("\n\n"); if ($Options{prologue}) { print $Options{prologue} . "\n"; } } sub end_output { return if ($Options{tableonly}); if ($Options{epilogue}) { print $Options{epilogue} . "\n"; } print("\n\n"); } sub parse_input { undef $days; undef $shades; undef $moons; undef $classes; undef $weeks; my $found_data = 0; while() { chomp; last if /^\# rem2ps2? begin$/; } my $line; # Month Year numdays firstday monday_first_flag $line = ; return 0 unless $line; chomp($line); ($Month, $Year, $Numdays, $Firstwkday, $Mondayfirst) = split(' ', $line); $Month =~ s/_/ /g; # Day names $line = ; return 0 unless $line; chomp($line); @Daynames = split(' ', $line); for (my $i=0; $i<7; $i++) { $Daynames[$i] =~ s/_/ /g; } # Prevmon prevlen $line = ; return 0 unless $line; chomp($line); ($Prevmon, $Prevlen) = split(' ', $line); $Prevmon =~ s/_/ /g; # Nextmon nextlen $line = ; return 0 unless $line; chomp($line); ($Nextmon, $Nextlen) = split(' ', $line); $Nextmon =~ s/_/ /g; $found_data = 1; my $class; if ($Options{nostyle}) { $class = ''; } else { $class = ' class="rem-entry"'; } while() { chomp; last if /^\# rem2ps2? end$/; next if /^\#/; my ($y, $m, $d, $special, $tag, $duration, $time, $body); if (m/^(\d*).(\d*).(\d*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*)$/) { ($y, $m, $d, $special, $tag, $duration, $time, $body) = ($1, $2, $3, $4, $5, $6, $7, $8); } elsif (/\{/) { my $obj = decode_json($_); next unless ($obj->{date} =~ /^(\d+)-(\d+)-(\d+)$/); $y = $1; $m = $2; $d = $3; $special = $obj->{passthru} || '*'; $tag = $obj->{tags} || '*'; $duration = $obj->{duration} || '*'; $time = $obj->{time} || '*'; $body = $obj->{body}; } else { next; } my $d1 = $d; $d1 =~ s/^0+//; $special = uc($special); if ($special eq 'HTML') { push(@{$days->[$d]}, $body); } elsif ($special eq 'HTMLCLASS') { $classes->[$d] = $body; } elsif ($special eq 'WEEK') { $body =~ s/^\s+//; $body =~ s/\s+$//; $weeks->{$d1} = $body; } elsif ($special eq 'MOON') { if ($body =~ /(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/) { my ($phase, $moonsize, $fontsize, $msg) = ($1, $2, $3, $4); $moons->[$d]->{'phase'} = $phase; $moons->[$d]->{'msg'} = $msg; } elsif ($body =~ /(\S+)/) { $moons->[$d]->{'phase'} = $1; $moons->[$d]->{'msg'} = ''; } } elsif ($special eq 'SHADE') { if ($body =~ /(\d+)\s+(\d+)\s+(\d+)/) { $shades->[$d] = sprintf("#%02X%02X%02X", ($1 % 256), ($2 % 256), ($3 % 256)); } elsif ($body =~ /(\d+)/) { $shades->[$d] = sprintf("#%02X%02X%02X", ($1 % 256), ($1 % 256), ($1 % 256)); } } elsif ($special eq 'COLOR' || $special eq 'COLOUR') { if ($body =~ /(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/) { my($r, $g, $b, $text) = ($1, $2, $3, $4); my $color = sprintf("style=\"color: #%02X%02X%02X;\"", $r % 256, $g % 256, $b % 256); push(@{$days->[$d]}, "" . escape_html($text) . '

'); } } elsif ($special eq '*') { push(@{$days->[$d]}, "" . escape_html($body) . '

'); } } return $found_data; } sub small_calendar { my($month, $monlen, $url, $first_col) = @_; if ($Mondayfirst) { $first_col--; if ($first_col < 0) { $first_col = 6; } } if ($Options{nostyle}) { print "\n"; print "\n"; print "\n"; } sub output_calendar { # Which column is 1st of month in? my $first_col = $Firstwkday; if ($Mondayfirst) { $first_col--; if ($first_col < 0) { $first_col = 6; } } # Last column my $last_col = ($first_col + $Numdays - 1) % 7; # Figure out how many rows my $number_of_rows = int(($first_col + $Numdays ) / 7 + 0.999); # Add a row for small calendars if necessary if ($first_col == 0 && $last_col == 6) { $number_of_rows++; } # Start the table my $class; if ($Options{nostyle}) { print '
"; } else { print "
\n"; print "\n"; print "\n"; my $class; if ($Options{nostyle}) { print ''; $class = ' align="right"'; } else { print ''; $class = ' class="rem-sc-hdr"'; } if (!$Mondayfirst) { print "" . substr($Daynames[0], 0, 1) . ''; } for (my $i=1; $i<7; $i++) { print "" . substr($Daynames[$i], 0, 1) . ''; } if ($Mondayfirst) { print "" . substr($Daynames[0], 0, 1) . ''; } print("\n"); my $col = 0; for (; $col<$first_col; $col++) { if ($col == 0) { print("\n"); } if ($Options{nostyle}) { print(""); } else { print(""); } } for (my $day=1; $day <= $monlen; $day++) { if ($col == 0) { print("\n"); } $col++; if ($Options{nostyle}) { print(""); } else { print(""); } if ($col == 7) { print("\n"); $col = 0; } } if ($col) { while ($col < 7) { if ($Options{nostyle}) { print(""); } else { print(""); } $col++; } print("\n"); } print("
"; } print "" if ($url); print $month; print "" if ($url); print "
  
$day$day
  
\n"); print "
' . "\n"; print ''; $class = ' width="14%"'; } else { print '
' . $Month . ' ' . $Year . '
' . "\n"; print ''; $class = ' class="rem-cal-hdr"'; } if (!$Mondayfirst) { print "" . $Daynames[0] . ''; } for (my $i=1; $i<7; $i++) { print "" . $Daynames[$i] . ''; } if ($Mondayfirst) { print "" . $Daynames[0] . ''; } print "\n"; # Start the calendar rows my $col = 0; if ($Options{nostyle}) { print "\n"; } else { print "\n"; } if ($first_col > 0) { small_calendar($Prevmon, $Prevlen, $Options{backurl}, ($Firstwkday - $Prevlen + 35) % 7); $col++; } if ($last_col == 6 && $first_col > 0) { small_calendar($Nextmon, $Nextlen, $Options{forwurl}, ($Firstwkday + $Numdays) % 7); $col++; } if ($Options{nostyle}) { $class = ' width="14%"'; } else { $class = ' class="rem-empty rem-empty-$number_of_rows-rows"'; } while ($col < $first_col) { print(" \n"); $col++; } for (my $day=1; $day<=$Numdays; $day++) { draw_day_cell($day, $number_of_rows); $col++; if ($col == 7) { $col = 0; print "\n"; if ($day < $Numdays) { if ($Options{nostyle}) { print "\n"; } else { print "\n"; } } } } if ($col) { while ($col < 7) { if ($col == 5) { if ($first_col == 0) { small_calendar($Prevmon, $Prevlen, $Options{backurl}, ($Firstwkday - $Prevlen + 35) % 7); } else { print(" \n"); } } elsif ($col == 6) { small_calendar($Nextmon, $Nextlen, $Options{forwurl}, ($Firstwkday + $Numdays) % 7); } else { print(" \n"); } $col++; } print "\n"; } # Add a row for small calendars if they were not yet done! if ($first_col == 0 && $last_col == 6) { if ($Options{nostyle}) { print "\n"; } else { print "\n"; } small_calendar($Prevmon, $Prevlen, $Options{backurl}, ($Firstwkday - $Prevlen + 35) % 7); for (my $i=0; $i<5; $i++) { print(" \n"); } small_calendar($Nextmon, $Nextlen, $Options{forwurl}, ($Firstwkday + $Numdays) % 7); print("\n"); } # End the table print "
' . $Month . ' ' . $Year . '
\n"; } sub draw_day_cell { my($day, $number_of_rows) = @_; my $shade = $shades->[$day]; my $week = ''; if (exists($weeks->{$day})) { $week = ' ' . $weeks->{$day}; } my $class; if ($Options{nostyle}) { $class = $classes->[$day] || ''; } else { $class = $classes->[$day] || "rem-cell rem-cell-$number_of_rows-rows"; } if ($shade) { $shade = " style=\"background: $shade;\""; } else { $shade = ""; } if ($class ne '') { print "\n"; } else { print "\n"; } if ($moons->[$day]) { my $phase = $moons->[$day]->{'phase'}; my $msg = $moons->[$day]->{'msg'}; $msg ||= ''; if ($msg ne '') { $msg = ' ' . escape_html($msg); } my $img; my $alt; my $title; if ($phase == 0) { if ($Options{pngs}) { $img = smoosh($Options{imgbase}, 'newmoon.png'); } else { $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAC6SURBVDiNpdNNbsIwFATgL0HKolchHKBX6yFaBOEyoPYUabvOIVKJRaCL2JX5TRNGGvnJ8ozGz89cYoElPvET+BX2yivn/1Bggw5HHMKa1h2qcPZC/JEIhvh+brIZIY6sorhMYo9hh3KGFzzfa84NZNjDt9OG/ZcH1BlaPE1IAG0+URhxzNGESKPFaHJs9Q0Ziww7HnvGeXSrJhis0jiFfjwnj3I0WRv+TKtr4hQl3lDrZ6QN9Wt654hfWfGDmBpUwDkAAAAASUVORK5CYII='; } $title = 'New Moon'; $alt = 'new'; } elsif ($phase == 1) { if ($Options{pngs}) { $img = smoosh($Options{imgbase}, 'firstquarter.png'); } else { $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADfSURBVDiNndM9TsNAFATgzy5yjZSAE85JBygETgENUPF3iBCitHAFQkcIhZ/Ryn9gRlrZmp2Z3ef3TBOHOMULPrDBMrhpi/4HI5xjix2+4nmJRbx/Yh7ahvkpRPVV4QDXwT3UQy46zGkAZDgK/iytefvHgCrkJsqZUH6cLnNbABSxd5Jhhf1IbkMXv8Qux7hH1Ic1xvk/jBWy6gavumvtwx7ectwZXkKh7MA95XgObeOtpI2U4zl0kGbpxgiPvwQUcXLrKFchc82f6Ur0PK49azOnmOI4TBu84zm4SV38DeIVYkrYJyNbAAAAAElFTkSuQmCC'; } $title = 'First Quarter'; $alt = '1st'; } elsif ($phase == 2) { if ($Options{pngs}) { $img = smoosh($Options{imgbase}, 'fullmoon.png'); } else { $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADlSURBVDiNrdNBUsJAEAXQlyw4hq4hwWPqTixET6ELkZ16CcAq7oFLqXExjaYgQVNlV/Viev7/6XT/4TjGuME7PiLXUatb8N8xwB12SFjiIXIZtU/MAntEfgvQE4YtHxhiHpjXQ5H7uLhEcaLLAleBvd0Xx9Ha/BdyU+Q5OBV5OKmj7a4YBWdSyNPe4aKHAHkzqcQZNj3JgnNexqE8heyIAulffuFF3kTfIVbBVeu/xoXGGsn2TLJJ/mqkafNiINszySYZdbS90GHlvcgsWktY4TFy7ecxTdvIzahxHQLbyFXUqkPwF2ASRNYgB/PXAAAAAElFTkSuQmCC'; } $alt = 'full'; $title = 'Full Moon'; } else { if ($Options{pngs}) { $img = smoosh($Options{imgbase}, 'lastquarter.png'); } else { $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADmSURBVDiNndMxTsNAEIXhzy5yCyQ6FAgcE7oQheQWUAAl5BIkREoZrgB0GFNkHBl7bURGsryaee/3jHeXdpxjghU+8InXyI0S+n0MMEeBEi+4jfV3vAvMQtsyL0J0j2GtViaeRRMyj8IlsgY8BSijE2Kur/hy09wHKMJrEolhwtwHKDHOsI4OLnoAXfl1jiNsOkR9keE4P8D4q4scbzg5xIxtjie709f1E7siC+9+Gx/8fxvPKtEsklcJSBdgWhcN8ByFR5z+AWgd5QpyE+OUWOJO+zJNU+Z6jHAdgHe7K73CuD5zFT9nCmRDIssCaAAAAABJRU5ErkJggg=='; } $alt = 'last'; $title = 'Last Quarter'; } if ($Options{nostyle}) { print("
\"$alt\"$msg
"); } else { print("
\"$alt\"$msg
"); } } if ($Options{nostyle}) { print "
$day$week
\n"; print "

 

\n"; } else { print "
$day$week
\n"; } if ($days->[$day]) { print(join("\n", @{$days->[$day]})); } print "\n"; } sub escape_html { my($in) = @_; $in =~ s/\&/\&/g; $in =~ s/\/\>/g; return $in; } parse_options(); if ($Options{help}) { usage(0); exit(0); } elsif ($Options{man}) { system("perldoc $0"); exit(0); } elsif ($Options{version}) { print "rem2html version $rem2html_version.\n"; exit(0); } if (-t STDIN) { ## no critic print STDERR "$TIDY_PROGNAME: Input should not come from a terminal.\n\n"; usage(1); } my $found_something = 0; while(1) { last if (!parse_input()); start_output() unless $found_something; $found_something = 1; output_calendar(); } if ($found_something) { end_output(); exit(0); } else { print STDERR "$TIDY_PROGNAME: Could not find any calendar data on STDIN.\n"; exit(1); } sub default_stylesheet { return <<'EOF'; table.rem-cal { font-family: helvetica, arial, sans-serif; font-size: 12pt; } table.rem-sc-table { font-family: helvetica, arial, sans-serif; font-size: 10pt; width: 95%; float: left; } caption.rem-cal-caption { font-size: 14pt; font-weight: bold; } th.rem-cal-hdr { width: 14%; border-style: solid; border-width: 1px; vertical-align: top; } td.rem-empty, td.rem-cell, td.rem-small-calendar { width: 14%; height: 7em; border-style: solid; border-width: 1px; vertical-align: top; } td.rem-today { width: 14%; height: 7em; border-style: solid; border-width: 2px; border-color: #EE3333; vertical-align: top; } table.rem-cal { width: 100%; border-collapse: collapse; } div.rem-daynumber { float: right; text-align: right; vertical-align: top; font-size: 14pt; } p.rem-entry { clear: both; } div.rem-moon { float: left; text-align: left; vertical-align: top; } th.rem-sc-hdr { text-align: right; } td.rem-sc-empty-cell, td.rem-sc-cell { text-align: right; width: 14%; } caption.rem-sc-caption { font-size: 12pt; } EOF }