214 lines
8.5 KiB
Bash
Executable File
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."
|