40C3: tours, schedule data, tour map and calendar entries
Some checks failed
/ build (pull_request) Successful in 1m16s
/ accessibility (pull_request) Failing after 2m25s

- tour-map shortcode: Leaflet map of the tour locations with routes from the
  Congress, from the map/tours sections of data/schedule/<event>.yaml; Leaflet
  and the OSMIC, Maki and Temaki icon sets vendored; 39C3 uMap data converted.
- calendar shortcode: per tour date, a pill with an .ics download (iCalendar)
  and a Google Calendar link; Font Awesome Free vendored, fa-icon partial.
- README sections for the calendar and tour map shortcodes and the map data.
This commit is contained in:
Stefan Bethke 2026-10-17 10:29:01 +02:00
commit 821290616a
3864 changed files with 15276 additions and 18 deletions

View file

@ -0,0 +1,10 @@
{{- /*
A Font Awesome icon as inline SVG, decorative (hidden from screen readers) and drawn in the
current text colour. Param: the icon as "style/name", e.g. "regular/calendar-plus"; the files
are in assets/vendor/fontawesome/. The SVG keeps its attribution comment (CC BY 4.0).
*/ -}}
{{- with resources.Get (printf "vendor/fontawesome/%s.svg" .) -}}
{{- replace .Content "<svg " `<svg class="fa-icon" aria-hidden="true" focusable="false" ` 1 | safeHTML -}}
{{- else -}}
{{- errorf "fa-icon: no icon %q in assets/vendor/fontawesome/" . -}}
{{- end -}}

View file

@ -0,0 +1,22 @@
{{- /*
One iCalendar (RFC 5545) content line: "NAME:value" plus CRLF, folded so that no line is
longer than 75 bytes (continuation lines start with a space). Params: name, value, text
(true to escape the value as TEXT: backslash, semicolon, comma and newlines).
*/ -}}
{{- $value := .value | default "" -}}
{{- if .text -}}
{{- $value = $value | replaceRE `\\` `\\` | replaceRE `;` `\;` | replaceRE `,` `\,` | replaceRE `\r?\n` `\n` -}}
{{- end -}}
{{- $out := "" -}}
{{- $line := "" -}}
{{- $bytes := 0 -}}
{{- range split (printf "%s:%s" .name $value) "" -}}
{{- if gt (add $bytes (len .)) 75 -}}
{{- $out = printf "%s%s\r\n " $out $line -}}
{{- $line = "" -}}
{{- $bytes = 1 -}}
{{- end -}}
{{- $line = printf "%s%s" $line . -}}
{{- $bytes = add $bytes (len .) -}}
{{- end -}}
{{- return printf "%s%s\r\n" $out $line -}}

View file

@ -0,0 +1,20 @@
{{- /*
Returns a map icon as a data: URI, or "" if there is none by that name.
The name is looked up in the vendored icon sets in assets/vendor/icons/, in the order
osmic, maki, temaki; a prefix like "maki:garden" picks the set.
*/ -}}
{{- $name := . -}}
{{- $sets := slice "osmic" "maki" "temaki" -}}
{{- with findRESubmatch `^(osmic|maki|temaki):(.+)$` . 1 -}}
{{- $sets = slice (index . 0 1) -}}
{{- $name = index . 0 2 -}}
{{- end -}}
{{- $uri := "" -}}
{{- range $sets -}}
{{- if not $uri -}}
{{- with resources.Get (printf "vendor/icons/%s/%s.svg" . $name) -}}
{{- $uri = printf "data:image/svg+xml;base64,%s" ((. | minify).Content | base64Encode) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- return $uri -}}

View file

@ -0,0 +1,10 @@
{{- /* Leaflet and the script for the tour-map shortcode, included by the base templates on pages that use it. */ -}}
{{- with resources.Get "vendor/leaflet/leaflet.css" | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- with resources.Get "vendor/leaflet/leaflet.js" | fingerprint }}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous" defer></script>
{{- end }}
{{- with resources.Get "js/tour-map.js" | minify | fingerprint }}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous" defer></script>
{{- end }}

View file

@ -0,0 +1,88 @@
{{- /*
Calendar links for one date of a tour, in a small pill with a calendar icon: an .ics file
(iCalendar), which Apple Calendar, Outlook, Thunderbird and most other calendar apps can open,
and a link that adds the date in Google Calendar, whose Android app can't open .ics files.
Meant for the list of dates on a tour page:
* Tag 1 (27.12.) **17:30 Uhr** {{< calendar date="2026-12-27" time="17:30" duration="210" >}}
Params: date (YYYY-MM-DD), time (HH:MM, local time in Hamburg), duration in minutes
(optional, else tours.<slug>.duration in data/schedule/<event>.yaml), title (optional, else the
title of this date in the schedule data, else the page title).
*/ -}}
{{- $page := .Page -}}
{{- $lang := $page.Language.Name -}}
{{- $other := cond (eq $lang "de") "en" "de" -}}
{{- $event := $page.Section -}}
{{- $slug := $page.File.ContentBaseName -}}
{{- $date := .Get "date" -}}
{{- $time := .Get "time" -}}
{{- if not (and (findRE `^\d{4}-\d{2}-\d{2}$` $date) (findRE `^\d{2}:\d{2}$` $time)) -}}
{{- errorf "calendar: date must be YYYY-MM-DD and time HH:MM, got %q %q: %s" $date $time .Position -}}
{{- end -}}
{{- $data := dict -}}
{{- with hugo.Data.schedule }}{{ with index . $event }}{{ $data = . }}{{ end }}{{ end -}}
{{- $tourData := dict -}}
{{- with $data.tours }}{{ with index . $slug }}{{ $tourData = . }}{{ end }}{{ end -}}
{{- /* This date in the schedule, for its title. */ -}}
{{- $slot := dict -}}
{{- range $data.days -}}
{{- if eq .date $date -}}
{{- range .tours }}{{ if and (eq .tour $slug) (eq .time $time) }}{{ $slot = . }}{{ end }}{{ end -}}
{{- end -}}
{{- end -}}
{{- $title := .Get "title" -}}
{{- if not $title }}{{ with $slot.title }}{{ $title = index . $lang | default (index . $other) }}{{ end }}{{ end -}}
{{- $title = $title | default $page.Title -}}
{{- $minutes := int (.Get "duration" | default ($tourData.duration | default 0)) -}}
{{- if not $minutes -}}
{{- warnidf "warning-calendar-duration" "calendar: no duration for %s on %s %s, the calendar entry has no end time: %s" $slug $date $time .Position -}}
{{- end -}}
{{- $start := time.AsTime (printf "%sT%s:00" $date $time) "Europe/Berlin" -}}
{{- $end := $start.Add (time.ParseDuration (printf "%dm" $minutes)) -}}
{{- $utc := "20060102T150405Z" -}}
{{- /* Everyone meets at the Hackertours desk, which is where the routes on the map start. */ -}}
{{- $location := i18n "calendar_meeting_point" -}}
{{- with $data.map }}{{ with .origin }}{{ with .name -}}
{{- $location = printf "%s, %s" $location (index . $lang | default (index . $other)) -}}
{{- end }}{{ end }}{{ end -}}
{{- /* The front matter summary: .Summary would render this page again from inside its own shortcode. */ -}}
{{- $summary := $page.Params.summary | default $page.Description | default "" -}}
{{- $description := printf "%s\n\n%s" ($page.RenderString $summary | plainify | htmlUnescape | strings.TrimSpace) $page.Permalink -}}
{{- $ics := printf "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//CCC Hansestadt Hamburg//Hackertours//%s\r\nCALSCALE:GREGORIAN\r\nMETHOD:PUBLISH\r\nBEGIN:VEVENT\r\n" (upper $lang) -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "UID" "value" (printf "%s-%s-%s-%s@%s" $event $slug $date (replace $time ":" "") (urls.Parse site.BaseURL).Host))) -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "DTSTAMP" "value" ($page.Lastmod.UTC.Format $utc))) -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "DTSTART" "value" ($start.UTC.Format $utc))) -}}
{{- if $minutes }}{{ $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "DTEND" "value" ($end.UTC.Format $utc))) }}{{ end -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "SUMMARY" "value" $title "text" true)) -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "LOCATION" "value" $location "text" true)) -}}
{{- with $data.map }}{{ with .origin }}{{ with .geo -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "GEO" "value" (printf "%v;%v" .lat .lon))) -}}
{{- end }}{{ end }}{{ end -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "DESCRIPTION" "value" $description "text" true)) -}}
{{- $ics = printf "%s%s" $ics (partial "ics-line.html" (dict "name" "URL" "value" $page.Permalink)) -}}
{{- $ics = printf "%sEND:VEVENT\r\nEND:VCALENDAR\r\n" $ics -}}
{{- $file := printf "%s-%s-%s.ics" $slug $date (replace $time ":" "") -}}
{{- $icsFile := resources.FromString (path.Join (strings.TrimPrefix "/" $page.RelPermalink) $file) $ics -}}
{{- /* The link texts are the same for every date, so their names for screen readers include the date. */ -}}
{{- $when := printf "%s %s" ($start | time.Format "Mon 2.1.") $time -}}
{{- $google := printf "https://calendar.google.com/calendar/render?%s" (querify
"action" "TEMPLATE"
"text" $title
"dates" (printf "%s/%s" ($start.UTC.Format $utc) ($end.UTC.Format $utc))
"details" $description
"location" $location
"ctz" "Europe/Berlin"
) -}}
<span class="tour-calendar">{{ partial "fa-icon.html" "regular/calendar-plus" -}}
<a href="{{ $icsFile.RelPermalink }}" download="{{ $file }}">{{ i18n "calendar_ics" }}<span class="visually-hidden"> {{ $when }}</span></a>
{{- /* */ -}}<span class="tour-calendar__separator" aria-hidden="true">|</span>
{{- /* */ -}}<a href="{{ $google }}">{{ i18n "calendar_google" }}<span class="visually-hidden"> {{ $when }}</span></a></span>
{{- /* chomp */ -}}

View file

@ -0,0 +1,80 @@
{{- /*
Map of an event's tour locations with routes from the Congress, on OpenStreetMap tiles.
Reads the map and tours sections of data/schedule/<event>.yaml (format in README.md).
Without JavaScript it shows a list of the locations with links to OpenStreetMap.
Usage: {{< tour-map >}} on a section page (the event is the section name),
or {{< tour-map 39c3 >}}.
*/ -}}
{{- $event := .Get 0 | default .Page.Section -}}
{{- $lang := .Page.Language.Name -}}
{{- $other := cond (eq $lang "de") "en" "de" -}}
{{- $data := dict -}}
{{- with hugo.Data.schedule }}{{ with index . $event }}{{ $data = . }}{{ end }}{{ end -}}
{{- /* Tour page titles start with the event name ("39C3 Rote Flora"); the map doesn't need it. */ -}}
{{- $prefix := printf `(?i)^%s\s+` $event -}}
{{- $tours := slice -}}
{{- range $slug, $tour := $data.tours -}}
{{- with $tour.geo -}}
{{- $title := $slug -}}
{{- $url := "" -}}
{{- with $.Page.GetPage $slug -}}
{{- $title = .LinkTitle | replaceRE $prefix "" -}}
{{- $url = .RelPermalink -}}
{{- else -}}
{{- warnf "tour-map: %s: no page for tour %q" $event $slug -}}
{{- end -}}
{{- $directions := "" -}}
{{- with $tour.directions -}}
{{- with index . $lang | default (index . $other) -}}
{{- $directions = $.Page.RenderString (dict "display" "block") . -}}
{{- end -}}
{{- end -}}
{{- $icon := "" -}}
{{- with $tour.icon -}}
{{- $icon = partial "tour-map-icon.html" . -}}
{{- if not $icon }}{{ warnf "tour-map: %s: unknown icon %q for tour %q" $event . $slug }}{{ end -}}
{{- end -}}
{{- $tours = $tours | append (dict
"title" $title
"url" $url
"lat" .lat
"lon" .lon
"color" ($tour.color | default "")
"icon" $icon
"directions" $directions
"route" ($tour.route | default slice)
) -}}
{{- end -}}
{{- end -}}
{{- $origin := dict -}}
{{- with $data.map }}{{ with .origin }}{{ with .geo -}}
{{- $name := "" -}}
{{- with $data.map.origin.name }}{{ $name = index . $lang | default (index . $other) }}{{ end -}}
{{- $icon := "" -}}
{{- with $data.map.origin.icon -}}
{{- $icon = partial "tour-map-icon.html" . -}}
{{- if not $icon }}{{ warnf "tour-map: %s: unknown icon %q for the origin" $event . }}{{ end -}}
{{- end -}}
{{- $origin = dict "name" $name "lat" .lat "lon" .lon "icon" $icon -}}
{{- end }}{{ end }}{{ end -}}
{{- if not $tours -}}
{{- warnidf "warning-tour-map-empty" "tour-map: no tours with geo in data/schedule/%s.yaml: %s" $event .Position -}}
{{- else -}}
{{- $tours = sort $tours "title" -}}
<div class="tour-map" data-tour-map="{{ jsonify (dict "origin" $origin "tours" $tours) }}">
<div class="tour-map__canvas" role="region" aria-label="{{ i18n "tour_map_label" }}"></div>
<ul class="tour-map__list">
{{- range $tours }}
<li>
{{- if .url }}<a href="{{ .url }}">{{ .title }}</a>{{ else }}{{ .title }}{{ end }}
(<a href="https://www.openstreetmap.org/?mlat={{ .lat }}&amp;mlon={{ .lon }}#map=17/{{ .lat }}/{{ .lon }}">{{ i18n "tour_map_osm" }}</a>)
{{- with .directions }}{{ . }}{{ end }}
</li>
{{- end }}
</ul>
</div>
{{- end -}}

View file

@ -14,6 +14,7 @@
{{ partial "head/extra.html" . }}
{{ block "head" . }}{{ end }}
{{ if .HasShortcode "schedule" }}{{ partial "schedule-script.html" . }}{{ end }}
{{ if .HasShortcode "tour-map" }}{{ partial "tour-map-script.html" . }}{{ end }}
{{ if .HasShortcode "gallery" }}{{ partial "gallery-script.html" . }}{{ end }}
</head>

View file

@ -14,6 +14,7 @@
{{ partial "head/extra.html" . }}
{{ block "head" . }}{{ end }}
{{ if .HasShortcode "schedule" }}{{ partial "schedule-script.html" . }}{{ end }}
{{ if .HasShortcode "tour-map" }}{{ partial "tour-map-script.html" . }}{{ end }}
{{ if .HasShortcode "gallery" }}{{ partial "gallery-script.html" . }}{{ end }}
</head>