| 1 | #!/usr/bin/env bash |
| 2 | # html-ppt :: new-deck.sh — scaffold a new deck from a template |
| 3 | # |
| 4 | # Usage: |
| 5 | # new-deck.sh <name> [output-parent-dir] [-t <template>] |
| 6 | # |
| 7 | # Creates <parent>/<name>/index.html with the asset references rewritten so |
| 8 | # they resolve from wherever the deck actually lands — at any depth, inside |
| 9 | # the skill tree or outside it. |
| 10 | # |
| 11 | # <output-parent-dir> absolute, or relative to the current directory. |
| 12 | # Defaults to <skill>/examples. |
| 13 | # -t, --template "deck" (default), a full-deck name (e.g. pitch-deck), |
| 14 | # a single-page name (e.g. arch-diagram), or a path to |
| 15 | # any .html file / full-deck directory. |
| 16 | |
| 17 | set -euo pipefail |
| 18 | |
| 19 | HERE="$(cd "$(dirname "$0")/.." && pwd)" |
| 20 | |
| 21 | NAME="" |
| 22 | PARENT="" |
| 23 | TEMPLATE_ARG="deck" |
| 24 | |
| 25 | while [[ $# -gt 0 ]]; do |
| 26 | case "$1" in |
| 27 | -t|--template) |
| 28 | TEMPLATE_ARG="${2:-}" |
| 29 | if [[ -z "$TEMPLATE_ARG" ]]; then |
| 30 | echo "error: --template needs a value" >&2 |
| 31 | exit 1 |
| 32 | fi |
| 33 | shift 2 |
| 34 | ;; |
| 35 | -h|--help) |
| 36 | sed -n '3,15p' "$0" | sed 's|^# \{0,1\}||' |
| 37 | exit 0 |
| 38 | ;; |
| 39 | -*) |
| 40 | echo "error: unknown option $1" >&2 |
| 41 | exit 1 |
| 42 | ;; |
| 43 | *) |
| 44 | if [[ -z "$NAME" ]]; then |
| 45 | NAME="$1" |
| 46 | elif [[ -z "$PARENT" ]]; then |
| 47 | PARENT="$1" |
| 48 | else |
| 49 | echo "error: unexpected argument $1" >&2 |
| 50 | exit 1 |
| 51 | fi |
| 52 | shift |
| 53 | ;; |
| 54 | esac |
| 55 | done |
| 56 | |
| 57 | if [[ -z "$NAME" ]]; then |
| 58 | echo "usage: new-deck.sh <name> [parent-dir] [-t <template>]" >&2 |
| 59 | exit 1 |
| 60 | fi |
| 61 | |
| 62 | # ---------------------------------------------------------------- template |
| 63 | # Accept a bare name (deck / a full-decks dir / a single-page layout) or an |
| 64 | # explicit path. A full-deck resolves to a directory and is copied whole. |
| 65 | resolve_template() { |
| 66 | local t="$1" |
| 67 | if [[ -e "$t" ]]; then printf '%s' "$t"; return; fi |
| 68 | if [[ "$t" == "deck" && -f "$HERE/templates/deck.html" ]]; then |
| 69 | printf '%s' "$HERE/templates/deck.html"; return |
| 70 | fi |
| 71 | if [[ -d "$HERE/templates/full-decks/$t" ]]; then |
| 72 | printf '%s' "$HERE/templates/full-decks/$t"; return |
| 73 | fi |
| 74 | if [[ -f "$HERE/templates/single-page/$t.html" ]]; then |
| 75 | printf '%s' "$HERE/templates/single-page/$t.html"; return |
| 76 | fi |
| 77 | if [[ -f "$HERE/templates/$t" ]]; then |
| 78 | printf '%s' "$HERE/templates/$t"; return |
| 79 | fi |
| 80 | return 1 |
| 81 | } |
| 82 | |
| 83 | if ! TEMPLATE="$(resolve_template "$TEMPLATE_ARG")"; then |
| 84 | echo "error: no template named '$TEMPLATE_ARG'" >&2 |
| 85 | echo " full-decks: $(ls "$HERE/templates/full-decks" 2>/dev/null | tr '\n' ' ')" >&2 |
| 86 | echo " single-page: see $HERE/templates/single-page/" >&2 |
| 87 | exit 1 |
| 88 | fi |
| 89 | |
| 90 | # ------------------------------------------------------------------ output |
| 91 | # Absolute parent is honoured as-is; a relative one resolves against the |
| 92 | # caller's current directory, which is what a shell user expects. Only the |
| 93 | # default lives inside the skill. |
| 94 | if [[ -z "$PARENT" ]]; then |
| 95 | PARENT_ABS="$HERE/examples" |
| 96 | elif [[ "$PARENT" == /* ]]; then |
| 97 | PARENT_ABS="$PARENT" |
| 98 | else |
| 99 | PARENT_ABS="$PWD/$PARENT" |
| 100 | fi |
| 101 | |
| 102 | OUT_DIR="$PARENT_ABS/$NAME" |
| 103 | if [[ -e "$OUT_DIR" ]]; then |
| 104 | echo "error: $OUT_DIR already exists" >&2 |
| 105 | exit 1 |
| 106 | fi |
| 107 | mkdir -p "$OUT_DIR" |
| 108 | OUT_DIR="$(cd "$OUT_DIR" && pwd)" # normalise ., .., symlinks |
| 109 | |
| 110 | # ---------------------------------------------------------------- rel path |
| 111 | # Relative path from the deck directory back to the skill root, so the asset |
| 112 | # links are correct at any depth and from outside the skill tree. |
| 113 | relpath() { |
| 114 | local target="${1%/}" base="${2%/}" up="" rest |
| 115 | while [[ "$base" != "/" && "$target" != "$base" && "$target" != "$base"/* ]]; do |
| 116 | base="$(dirname "$base")" |
| 117 | up="../$up" |
| 118 | done |
| 119 | rest="${target#"$base"}" |
| 120 | rest="${rest#/}" |
| 121 | up="${up%/}" |
| 122 | if [[ -z "$up" ]]; then printf '%s' "${rest:-.}" |
| 123 | elif [[ -z "$rest" ]]; then printf '%s' "$up" |
| 124 | else printf '%s/%s' "$up" "$rest" |
| 125 | fi |
| 126 | } |
| 127 | |
| 128 | REL="$(relpath "$HERE" "$OUT_DIR")" |
| 129 | |
| 130 | # Rewrite any run of ../ in front of assets/ to the computed prefix, so the |
| 131 | # same logic works for templates/deck.html (../assets/), single-page |
| 132 | # (../../assets/) and full-decks (../../../assets/) alike. |
| 133 | rewrite_assets() { |
| 134 | local file="$1" |
| 135 | sed -E -i.bak "s#([\"'(])(\.\./)*assets/#\1${REL}/assets/#g" "$file" |
| 136 | rm -f "$file.bak" |
| 137 | } |
| 138 | |
| 139 | if [[ -d "$TEMPLATE" ]]; then |
| 140 | cp -R "$TEMPLATE/." "$OUT_DIR/" |
| 141 | while IFS= read -r f; do rewrite_assets "$f"; done \ |
| 142 | < <(find "$OUT_DIR" -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \)) |
| 143 | else |
| 144 | cp "$TEMPLATE" "$OUT_DIR/index.html" |
| 145 | rewrite_assets "$OUT_DIR/index.html" |
| 146 | fi |
| 147 | |
| 148 | if [[ ! -f "$OUT_DIR/index.html" ]]; then |
| 149 | echo "error: template produced no index.html in $OUT_DIR" >&2 |
| 150 | exit 1 |
| 151 | fi |
| 152 | |
| 153 | # Join a relative reference onto a directory the way a browser resolves it |
| 154 | # against a file:// URL: purely lexically. The shell's own ".." follows |
| 155 | # symlinks instead (/tmp -> /private/tmp), which would disagree. |
| 156 | lexjoin() { |
| 157 | local p="$1/$2" out="" seg oldifs="$IFS" |
| 158 | set -f; IFS='/' |
| 159 | for seg in $p; do |
| 160 | case "$seg" in |
| 161 | ''|.) ;; |
| 162 | ..) out="${out%/*}" ;; |
| 163 | *) out="$out/$seg" ;; |
| 164 | esac |
| 165 | done |
| 166 | IFS="$oldifs"; set +f |
| 167 | printf '%s' "${out:-/}" |
| 168 | } |
| 169 | |
| 170 | # List every local href / src / url() reference in a file. The previous version |
| 171 | # of this check only enumerated references containing "assets/", which skipped |
| 172 | # the one reference every bundled deck has and every move breaks: its own |
| 173 | # <link rel="stylesheet" href="style.css">. Across the 15 full-decks that was |
| 174 | # 53 of 68 references actually checked — the 15 it missed were one style.css |
| 175 | # per deck. |
| 176 | # |
| 177 | # Blank lines are dropped by a separate sed stage, not by a '^$' branch inside |
| 178 | # the protocol grep. A fragment-only reference (url(#arrow), href="#top") |
| 179 | # collapses to an empty string once the #… is stripped, and under ugrep — |
| 180 | # which ships as `grep` on some machines — '^$' as one branch of an ERE |
| 181 | # alternation does not reliably match those empty lines (measured: 4 of 5 |
| 182 | # survived). The empty string then resolves to the deck directory, which |
| 183 | # exists, so it was silently counted as a verified reference. |
| 184 | list_refs() { |
| 185 | grep -oE "(href|src)=(\"[^\"]*\"|'[^']*')|url\([^)]*\)" "$1" \ |
| 186 | | sed -E 's/^(href|src)=//; s/^url\(//; s/\)$//' \ |
| 187 | | tr -d "\"'" \ |
| 188 | | sed -E 's/[?#].*$//' \ |
| 189 | | grep -vE '^[a-zA-Z][a-zA-Z0-9+.-]*:|^//' \ |
| 190 | | sed '/^[[:space:]]*$/d' \ |
| 191 | | sort -u |
| 192 | } |
| 193 | |
| 194 | # Self-check: every local reference must resolve to a real file. A broken path |
| 195 | # here is exactly the bug this script used to ship (#19) — fail loudly. |
| 196 | MISSING=0 |
| 197 | CHECKED=0 |
| 198 | while IFS= read -r ref; do |
| 199 | CHECKED=$((CHECKED + 1)) |
| 200 | if [[ ! -e "$(lexjoin "$OUT_DIR" "$ref")" ]]; then |
| 201 | echo "error: unresolved reference: $ref" >&2 |
| 202 | MISSING=1 |
| 203 | fi |
| 204 | done < <(list_refs "$OUT_DIR/index.html") |
| 205 | [[ $MISSING -eq 0 ]] || exit 1 |
| 206 | |
| 207 | echo "✔ created $OUT_DIR/index.html" |
| 208 | echo " template: $TEMPLATE" |
| 209 | echo " assets: $REL/assets/" |
| 210 | echo " refs: $CHECKED local reference(s) verified" |
| 211 | echo "" |
| 212 | echo "next steps:" |
| 213 | echo " open $OUT_DIR/index.html" |
| 214 | echo " # press T to cycle themes, ← → to navigate, O for overview" |
| 215 | echo "" |
| 216 | echo " # render to PNG:" |
| 217 | echo " $HERE/scripts/render.sh $OUT_DIR/index.html all" |
| 218 |