levineuwirth.org/tools/add-popup-source.sh

214 lines
8.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# add-popup-source.sh — Scaffold a new popup provider end-to-end.
#
# Prompts for the handful of facts unique to a new source (name, label,
# a sample URL), then:
# 1. Probes the upstream for CORS support and content-type.
# 2. Prints a PROVIDERS entry stub you paste into static/js/popups.js.
# 3. If CORS-broken, prints + (on confirmation) appends an nginx
# location block to nginx/popup-proxy.conf.
# 4. Prints a remaining-steps checklist (icon SVG, CSS, CSP comment).
#
# The parse() body is left as a TODO — writing it requires eyes on the
# actual API response, which this tool also fetches and pretty-prints so
# you can inspect the response shape without a second terminal.
#
# Never writes to static/js/popups.js automatically — that file has too
# much surrounding structure to edit blindly. Copy-paste is safer.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
POPUPS_JS="$REPO_ROOT/static/js/popups.js"
NGINX_CONF="$REPO_ROOT/nginx/popup-proxy.conf"
ORIGIN="https://levineuwirth.org"
# ── helpers ──────────────────────────────────────────────────────────
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
dim() { printf '\033[2m%s\033[0m\n' "$*"; }
warn() { printf '\033[33m%s\033[0m\n' "$*" >&2; }
prompt() { local v; read -r -p "$1 " v; printf '%s' "$v"; }
usage() {
cat <<EOF
Usage: tools/add-popup-source.sh [--help]
Interactive scaffold for a new popups.js provider. Asks a few
questions, probes the upstream, and prints the code you paste into
the provider table (and, if needed, an nginx reverse-proxy block).
Requires: curl, jq (for JSON pretty-printing), xmllint (optional).
EOF
}
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
# ── interactive prompts ──────────────────────────────────────────────
bold "── new popup provider ──"
NAME=$(prompt "slug (lowercase, used as class + data-popup-source key, e.g. 'zenodo'):")
[[ -z "$NAME" ]] && { warn "slug required"; exit 1; }
LABEL=$(prompt "display label (e.g. 'Zenodo'):")
[[ -z "$LABEL" ]] && LABEL="$NAME"
SAMPLE_URL=$(prompt "sample link URL (the kind readers will click):")
[[ -z "$SAMPLE_URL" ]] && { warn "sample URL required"; exit 1; }
API_URL=$(prompt "sample API URL (leave blank if you haven't found it yet):")
# ── CORS + content-type probe ────────────────────────────────────────
echo
bold "── probing upstream ──"
if [[ -n "$API_URL" ]]; then
HEADERS=$(curl -sSI -H "Origin: $ORIGIN" "$API_URL" 2>&1 || true)
# grep returns 1 when no match — tolerate that under `set -e` + pipefail.
CORS=$(printf '%s\n' "$HEADERS" | grep -i 'access-control-allow-origin' | head -1 | tr -d '\r' || true)
CT=$(printf '%s\n' "$HEADERS" | grep -i '^content-type' | head -1 | tr -d '\r' | awk '{print tolower($2)}' || true)
if [[ -n "$CORS" ]]; then
echo " CORS ✓ $CORS"
NEEDS_PROXY=0
else
warn " CORS ✗ none — upstream needs a reverse proxy"
NEEDS_PROXY=1
fi
echo " Content-Type: ${CT:-unknown}"
case "$CT" in
*xml*|*atom*) FETCH_TYPE=xml ;;
*json*) FETCH_TYPE=json ;;
*) FETCH_TYPE=json; warn " (unrecognized type — defaulting to json)" ;;
esac
echo " fetchType → $FETCH_TYPE"
else
warn " no API URL supplied; assuming json + CORS-OK (edit by hand if wrong)"
FETCH_TYPE=json
NEEDS_PROXY=0
fi
# ── sample response dump (helps write the parser) ────────────────────
if [[ -n "$API_URL" ]]; then
echo
bold "── sample response (first ~40 lines) ──"
RAW=$(curl -sS "$API_URL" 2>&1 || true)
if [[ "$FETCH_TYPE" == json ]] && command -v jq >/dev/null; then
printf '%s\n' "$RAW" | jq -C . 2>/dev/null | head -40 || printf '%s\n' "$RAW" | head -40
elif [[ "$FETCH_TYPE" == xml ]] && command -v xmllint >/dev/null; then
printf '%s\n' "$RAW" | xmllint --format - 2>/dev/null | head -40 || printf '%s\n' "$RAW" | head -40
else
printf '%s\n' "$RAW" | head -40
fi
fi
# ── proxy prefix + upstream host derivation ──────────────────────────
if [[ "$NEEDS_PROXY" -eq 1 ]]; then
UPSTREAM_HOST=$(printf '%s' "$API_URL" | awk -F/ '{print $3}')
UPSTREAM_PATH=$(printf '%s' "$API_URL" | awk -F/ 'BEGIN{OFS="/"} {$1=""; $2=""; $3=""; print}' | sed 's|^///||')
PROXY_PATH="/proxy/$NAME/"
PROXY_API_URL="$PROXY_PATH${UPSTREAM_PATH%%\?*}"
[[ "$API_URL" == *"?"* ]] && PROXY_API_URL="$PROXY_API_URL?${API_URL#*\?}"
else
UPSTREAM_HOST=""
PROXY_API_URL="$API_URL"
fi
# ── PROVIDERS entry stub ─────────────────────────────────────────────
echo
bold "── paste into static/js/popups.js (PROVIDERS array) ──"
dim " Insertion order = match priority; pick a spot before less-specific entries."
echo
cat <<EOF
/* $LABEL — TODO: one-line description. */
{
name: '$NAME', label: '$LABEL',
match: /TODO-regex-for-public-URL/,
fetchType: '$FETCH_TYPE',
url: function (ctx) {
return '${PROXY_API_URL:-TODO-upstream-URL}'; // uses ctx.match / ctx.href
},
parse: function (data) {
// TODO: map response to { title, authors?, meta?, abstract?, stats? }
if (!data || !data.TITLE_FIELD) return null;
return {
title: data.TITLE_FIELD,
authors: data.AUTHORS_FIELD || [],
abstract: data.ABSTRACT_FIELD || ''
};
}
},
EOF
# ── nginx proxy block ────────────────────────────────────────────────
if [[ "$NEEDS_PROXY" -eq 1 ]]; then
echo
bold "── nginx block for $NGINX_CONF ──"
NGINX_BLOCK=$(cat <<EOF
# ── $LABEL ───────────────────────────────────────────────────────────
# TODO: note the upstream + why it's CORS-broken + cache justification.
location /proxy/$NAME/ {
set \$upstream_$NAME $UPSTREAM_HOST;
proxy_pass https://\$upstream_$NAME/;
proxy_set_header Host \$upstream_$NAME;
proxy_set_header User-Agent "levineuwirth.org popup-proxy (ln@levineuwirth.org)";
proxy_ssl_server_name on;
proxy_cache popup_proxy;
proxy_cache_valid 200 30d;
proxy_cache_valid any 5m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status \$upstream_cache_status always;
add_header Access-Control-Allow-Origin "\$scheme://\$host" always;
}
EOF
)
printf '%s\n' "$NGINX_BLOCK"
echo
ANSWER=$(prompt "append this block to nginx/popup-proxy.conf now? [y/N]")
if [[ "$ANSWER" =~ ^[Yy] ]]; then
printf '%s\n' "$NGINX_BLOCK" >> "$NGINX_CONF"
echo " appended to $NGINX_CONF"
else
echo " skipped — paste manually when ready"
fi
fi
# ── remaining-steps checklist ────────────────────────────────────────
echo
bold "── remaining manual steps ──"
cat <<EOF
1. In static/js/popups.js: paste the PROVIDERS entry into the table
(it lives after the provider helpers; order = match priority) and
fill the TODO regex + parse() body.
2. In static/css/popups.css: add an icon rule (copy any existing
.popup-source[data-popup-source="…"]::before block and swap in the
$NAME key + the mask-image path).
3. Add static/images/link-icons/$NAME.svg — used by both the inline
link icon and the popup source label. Monochrome SVG, currentColor.
4. In build/Filters/Links.hs: add a clause to \`domainIcon\` so links
to this source get data-link-icon="$NAME" at build time.
EOF
if [[ "$NEEDS_PROXY" -eq 0 && -n "$UPSTREAM_HOST" ]]; then
echo " 5. In static/js/popups.js top-comment: add $UPSTREAM_HOST to the"
echo " connect-src CSP list."
fi
echo
dim "done."