calendar2html

#!/bin/sh -eu
IFS='	'
# converts list of periodic calendar entries from calendar(1) into HTML
# © 2018 Nils Dagsson Moskopp (erlehmann) – license: AGPLv3+
case $# in
 1) break ;;
 *) printf 'Usage: calendar2html calendar >calendar.html\n' >&2 && exit 1 ;;
esac

if [ ! -e "$1" ]; then
 printf '\n' >&2;
 exit 1
fi

printf '<!DOCTYPE html>
<meta charset=utf-8>
<title>%s</title>
<style>%s</style>
' "$1" '
* {
 margin:0;
 padding:0;
}
.calendar {
 display:flex;
 flex-direction:row;
 flex-wrap:wrap;
 align-items:stretch;
 width:100%;
 justify-content:flex-start;
}
.day {
 border-collapse:collapse;
 box-sizing:border-box;
 display:block;
 outline:1px solid #ccc;
 overflow-x:hidden;
 width:100%;
}
.day:target {
 outline:2px solid #48f;
 outline-offset:-3px;
}
.day caption {
 display:block;
 height:2em;
 overflow:hidden;
 padding:0.5rem;
 text-align:left;
}
.day caption a {
 color:#000;
 background:#fff;
 text-decoration:none;
}
.day caption .d,
.day caption .b,
.day caption .Y,
.day caption .a {
 box-sizing:border-box;
 display: inline-block;
 height:2em;
}
.day caption .d,
.day caption .a {
 color:#000;
 background:#eee;
 border:0.5em solid #eee;
 border-radius: 2em;
 text-align:center;
}
.day.today caption .d,
.day.today caption .a {
 color:#fff;
 background:#48f;
 border-color:#48f;
}
.day caption .d {
 width:2em;
}
.day caption .a {
 display:block;
 float:right;
}
.day tbody,
.day tr {
 display:block;
 min-width:100%;
}
.day tr:nth-of-type(2n) {
 background:#fff;
}
.day tr:nth-of-type(2n+1) {
 background:#eee;
}
.day tr td {
 padding:0.5em;
 vertical-align:top;
}
@media screen and (min-width: 42rem) {
 .day {
  font-size:0.75rem;
  min-width:6rem;
  min-height:4rem;
  width:14.28%;
 }
 .day:target {
  width:100%;
 }
 .day caption .a {
  display:none;
 }
 .day:target caption .a {
  display:block;
 }
 .day.u1:first-child,
 .day:target+.day.u1 {
  margin-left:0;
 }
 .day.u2:first-child,
 .day:target+.day.u2 {
  margin-left:14.28%;
 }
 .day.u3:first-child,
 .day:target+.day.u3 {
  margin-left:28.57%;
 }
 .day.u4:first-child,
 .day:target+.day.u4 {
  margin-left:42.85%;
 }
 .day.u5:first-child,
 .day:target+.day.u5 {
  margin-left:57.14%;
 }
 .day.u6:first-child,
 .day:target+.day.u6 {
  margin-left:71.42%;
 }
 .day.u7:first-child,
 .day:target+.day.u7 {
  margin-left:85.71%;
 }
}
@media screen and (min-width: 84rem) {
 .day {
  font-size:1rem !important;
  min-width:12rem;
  min-height:4rem;
 }
 .day caption .a {
  display:block !important;
 }
 .day:target {
  width:14.28% !important;
 }
 .day:target+.day {
  margin-left:0 !important;
 }
}
'

printf '<div class=calendar>'

cpp() {
 abspath=$( readlink -f $1 )
 cd ${abspath%/*}
 commands=$(
  sed -n 's:#include.*"\(.*\)":/&/ { \nr \1\nd }:p' <$1 |\
   sed '/#include.*/ { s_/_\\/_g; s_^\\/_/_; s_\\/ {_/ {_ }'
 )
 sed -e "$commands" "$1"
}

html_escape() {
 sed -e "s/\&/\&amp\;/g;s/</\&lt\;/g;s/>/\&gt\;/g;"
}

normalize_datetimes() {
 while read -r time note; do
  case "$time" in
   '#'*) continue ;;
  esac
  case "$note" in
   '') continue ;;
  esac
  case "$time" in
   ????-??-??)
    ts=$( date +%s -d "$time 00:00:00" )
    ;;
   *)
    set +e
    ts=$( date +%s -d "$time" )
    if [ "$?" -eq "1" ]; then
     set -e
     continue
    fi
    set -e
    ;;
  esac
  datetime=$( date '+%Y-%m-%d %H:%M' -d @$ts )
  printf '%s\t%s\n' "$datetime" "$note"
 done
}

tmpfile=$( mktemp || printf '%s.tmp' "$1" )
cpp "$1" |normalize_datetimes |sort -n >"$tmpfile"

date_start=$( head -n1 "$tmpfile" |cut -d' ' -f1 )
date_end=$( tail -n1 "$tmpfile" |cut -d' ' -f1 )

ts_start=$( date +%s -d "$date_start 00:00:00" )
ts_end=$( date +%s -d "$date_end 00:00:00" )
ts_cur=$ts_start

while [ $ts_cur -ge $ts_start ] && [ $ts_cur -le $ts_end ]; do
 date_strings=$( date '+%Y-%m-%d	%e	%b	%Y	%a	%u' -d @$ts_cur )
 weekday_cur=${date_strings##*	}
 date_cur=${date_strings%%	*}
 classes="day u$weekday_cur"
 printf '<table class="%s" id="%s">\n' "$classes" "$date_cur"
 printf '<caption><a href="#%s"><span class=d>%s</span> <span class=b>%s</span> <span class=Y>%s</span></a> <span class="a">%s</span></caption>\n' ${date_strings%	*}
 while read -r datetime_row note; do
  date_row=${datetime_row% *}
  time_row=${datetime_row#* }
  case $time_row in
   00:00) time_row= ;;
  esac
  if [ "$date_cur" = "$date_row" ]; then
   printf '<tr><td>%s<td>%s\n' "$time_row" $( printf '%s' "$note" |html_escape )
  fi
 done <"$tmpfile"
 printf '</table>\n'
 ts_cur=$(( ts_cur + 86400 ))
done
printf '</div>
<p>
<small>This page was generated by <a download="calendar2html" href="data:text/x-shellscript;charset=utf-8;base64,%s">calendar2html</a>. <i>calendar2html</i> is free software released under the terms of the <a href="http://www.gnu.org/licenses/agpl-3.0.txt"><abbr title="GNU is Not Unix">GNU</abbr>&nbsp;<abbr title="Affero General Public License">AGPL</abbr></a>, either version 3 of the License, or (at your option) any later version.</small>
<script>
var date_today = new Date().toISOString().split("T")[0];
document.getElementById(date_today).className += " today";
if (location.hash == "") {
  location.hash = "#" + date_today;
}
</script>
' "$( base64 "$0" |tr -d '\n' )"

rm "${tmpfile}"