#!/usr/bin/env bash
#
# Downloads the two typefaces the site uses and writes a local @font-face
# stylesheet against them.
#
# The site must not link to fonts.googleapis.com. Loading a font from Google
# transmits the visitor's IP address to a third country on every page view,
# which a German court has already held to be an unlawful disclosure under the
# GDPR (LG München I, 3 O 17493/20, 20 Jan 2022). Self-hosting removes the
# transfer entirely, which is also why the site needs no cookie banner: there
# is nothing to consent to.
#
# IBM Plex is licensed under the SIL Open Font License 1.1, which permits
# redistribution, including bundled with a website. See OFL.txt.
#
# Re-run this after changing which weights the CSS asks for. It is not run at
# page load and it is not run by any visitor — it is a build step, and its
# output (the .woff2 files and fonts.css) is what gets deployed.
#
# Usage:  ./fetch-fonts.sh
set -euo pipefail

cd "$(dirname "$0")"

# A desktop UA, so Google serves woff2 rather than the truetype fallback it
# hands to clients it does not recognise.
readonly USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'

readonly FAMILY_QUERY='family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono:wght@500&display=swap'

# Only the Latin subsets are kept. The site is English with a German
# Impressum, and every character either needs — including the umlauts and the
# eszett — lives in the `latin` range (U+0000-00FF). `latin-ext` is kept for
# names and place names that reach past it. Shipping Cyrillic, Greek and
# Vietnamese as well would roughly triple the font payload for characters no
# page here contains.
readonly WANTED_SUBSETS='latin latin-ext'

echo "Fetching stylesheet from Google Fonts (build time only, never at page load)..."
curl --fail --silent --show-error --max-time 30 \
  -A "$USER_AGENT" \
  "https://fonts.googleapis.com/css2?${FAMILY_QUERY}" \
  -o remote.css

echo "Extracting the ${WANTED_SUBSETS// /, } faces..."

# IBM Plex is served as a variable font: every requested weight of a family
# resolves to the same URL, and Google simply repeats the @font-face block
# once per weight. Downloading that file three times and declaring it three
# times would triple the bytes for nothing, so faces are grouped by the file
# they actually point at and emitted once with a weight *range*. The browser
# then interpolates the wght axis, which is what makes 600 and 700 different
# from 400 off a single file.
awk -v wanted="$WANTED_SUBSETS" '
  BEGIN {
    split(wanted, subsetList, " ")
    for (i in subsetList) keepSubset[subsetList[i]] = 1
  }
  /^\/\* .* \*\/$/ { currentSubset = $2; next }
  /@font-face/     { inBlock = 1; family = ""; weight = ""; remoteUrl = "" }
  inBlock {
    if ($1 == "font-family:") {
      family = $0
      sub(/^[^\x27]*\x27/, "", family)   # drop everything up to the opening quote
      sub(/\x27.*$/, "", family)         # drop the closing quote and the semicolon
    }
    if ($1 == "font-weight:") { weight = $2; sub(/;/, "", weight) }
    if ($1 == "src:") {
      remoteUrl = $0
      sub(/^[^(]*\(/, "", remoteUrl)
      sub(/\).*$/, "", remoteUrl)
    }
  }
  /^}/ && inBlock {
    inBlock = 0
    if (!(currentSubset in keepSubset)) next

    # One record per distinct file. Weights collapse into a min/max range.
    key = remoteUrl
    if (!(key in seen)) {
      seen[key] = 1
      order[++faceCount] = key
      urlOf[key] = remoteUrl
      familyOf[key] = family
      subsetOf[key] = currentSubset
      rangeOf[key] = unicodeRange
      minWeight[key] = weight
      maxWeight[key] = weight
    }
    if (weight + 0 < minWeight[key] + 0) minWeight[key] = weight
    if (weight + 0 > maxWeight[key] + 0) maxWeight[key] = weight
  }
  # unicode-range sits inside the block, above its closing brace, so it is
  # simply stashed here and attached when the block is registered. Attaching it
  # to the most recently registered face instead would give every face the
  # range of the one after it.
  /unicode-range:/ {
    unicodeRange = $0
    sub(/^[^:]*:[ \t]*/, "", unicodeRange)
    sub(/;[ \t]*$/, "", unicodeRange)
  }
  END {
    print "/* Generated by fetch-fonts.sh. Do not edit by hand. */"
    print "/* IBM Plex, SIL Open Font License 1.1 - see OFL.txt. */"
    print "/* Served from this origin only: no request leaves the visitor\x27s browser for a font. */"
    for (i = 1; i <= faceCount; i++) {
      key = order[i]
      slug = tolower(familyOf[key])
      gsub(/ /, "-", slug)
      localFile = slug "-" subsetOf[key] ".woff2"

      print urlOf[key] > "download-list.txt"
      print localFile  >> "download-list.txt"

      weightDescriptor = (minWeight[key] == maxWeight[key]) \
        ? minWeight[key] \
        : minWeight[key] " " maxWeight[key]

      printf "\n/* %s, %s */\n", familyOf[key], subsetOf[key]
      print  "@font-face {"
      printf "  font-family: \x27%s\x27;\n", familyOf[key]
      print  "  font-style: normal;"
      printf "  font-weight: %s;\n", weightDescriptor
      print  "  font-display: swap;"
      printf "  src: url(\"%s\") format(\"woff2\");\n", localFile
      printf "  unicode-range: %s;\n", rangeOf[key]
      print  "}"
    }
  }
' remote.css > fonts.css

echo "Downloading the .woff2 files..."
# download-list.txt is url/filename pairs, one per line.
while IFS= read -r remoteUrl && IFS= read -r localFile; do
  printf '  %s\n' "$localFile"
  curl --fail --silent --show-error --max-time 30 -A "$USER_AGENT" "$remoteUrl" -o "$localFile"
done < download-list.txt

rm -f remote.css download-list.txt

echo
echo "Done. $(ls -1 ./*.woff2 | wc -l | tr -d ' ') files, $(du -ch ./*.woff2 | tail -1 | cut -f1) of fonts."
if grep -q 'https://' fonts.css; then
  echo "WARNING: fonts.css still contains a remote URL. That is the GDPR problem this script exists to remove."
  exit 1
fi
echo "fonts.css references local files only."
