返回 html-ppt-skill
new-deck.sh
根目录 / scripts / new-deck.sh
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
218 lines BASH