diff options
-rwxr-xr-x | vienna | 428 |
1 files changed, 215 insertions, 213 deletions
diff --git a/vienna b/vienna index 8c3c92f..c7b4ffe 100755 --- a/vienna +++ b/vienna | |||
@@ -6,7 +6,7 @@ | |||
6 | ### Entry point | 6 | ### Entry point |
7 | 7 | ||
8 | usage() { | 8 | usage() { |
9 | cat <<EOF | 9 | cat <<EOF |
10 | VIENNA: a tiny, tasty ssg | 10 | VIENNA: a tiny, tasty ssg |
11 | by C. Duckworth <acdw@acdw.net> | 11 | by C. Duckworth <acdw@acdw.net> |
12 | 12 | ||
@@ -37,131 +37,132 @@ which by default is ./.vienna.sh. vienna uses heredoc-inspired templating, so | |||
37 | you can include shell snippets and variables by doubling the dollar signs. | 37 | you can include shell snippets and variables by doubling the dollar signs. |
38 | 38 | ||
39 | EOF | 39 | EOF |
40 | exit "${1:-0}" | 40 | exit "${1:-0}" |
41 | } | 41 | } |
42 | 42 | ||
43 | configure() { | 43 | configure() { |
44 | ## Set up environment | 44 | ## Set up environment |
45 | DOMAIN="${VIENNA_DOMAIN:-https://www.example.com}" | 45 | DOMAIN="${VIENNA_DOMAIN:-https://www.example.com}" |
46 | TMPD="${VIENNA_TMPD:-/tmp/vienna}" | 46 | TMPD="${VIENNA_TMPD:-/tmp/vienna}" |
47 | WORKD="${VIENNA_WORKD:-$PWD}" | 47 | WORKD="${VIENNA_WORKD:-$PWD}" |
48 | OUTD="${VIENNA_OUTD:-out}" | 48 | OUTD="${VIENNA_OUTD:-out}" |
49 | CONFIG="${VIENNA_CONFIG:-./.vienna.sh}" | 49 | CONFIG="${VIENNA_CONFIG:-./.vienna.sh}" |
50 | # Templates | 50 | # Templates |
51 | PAGE_TEMPLATE="${VIENNA_PAGE_TEMPLATE:-.page.tmpl.html}" | 51 | PAGE_TEMPLATE="${VIENNA_PAGE_TEMPLATE:-.page.tmpl.html}" |
52 | INDEX_TEMPLATE="${VIENNA_INDEX_TEMPLATE:-.index.tmpl.html}" | 52 | INDEX_TEMPLATE="${VIENNA_INDEX_TEMPLATE:-.index.tmpl.html}" |
53 | FEED_TEMPLATE="${VIENNA_FEED_TEMPLATE:-.feed.tmpl.xml}" | 53 | FEED_TEMPLATE="${VIENNA_FEED_TEMPLATE:-.feed.tmpl.xml}" |
54 | # File extensions | 54 | # File extensions |
55 | PAGE_RAW_EXT="${VIENNA_PAGE_RAW_EXT:-htm}" | 55 | PAGE_RAW_EXT="${VIENNA_PAGE_RAW_EXT:-htm}" |
56 | # Logging | 56 | # Logging |
57 | LOG=true | 57 | LOG=true |
58 | ## Parse command line arguments | 58 | ## Parse command line arguments |
59 | while getopts C:c:d:ho:q opt; do | 59 | while getopts C:c:d:ho:q opt; do |
60 | case "$opt" in | 60 | case "$opt" in |
61 | C) WORKD="$OPTARG" ;; | 61 | C) WORKD="$OPTARG" ;; |
62 | c) | 62 | c) |
63 | CONFIG="$OPTARG" | 63 | CONFIG="$OPTARG" |
64 | # To error later if a config is specified on the command | 64 | # To error later if a config is specified on the command |
65 | # line but doesn't exist. | 65 | # line but doesn't exist. |
66 | CONFIG_ARG=1 | 66 | CONFIG_ARG=1 |
67 | ;; | 67 | ;; |
68 | d) DOMAIN="$OPTARG" ;; | 68 | d) DOMAIN="$OPTARG" ;; |
69 | h) usage 0 ;; | 69 | h) usage 0 ;; |
70 | o) OUTD="$OPTARG" ;; | 70 | o) OUTD="$OPTARG" ;; |
71 | q) LOG=false ;; | 71 | q) LOG=false ;; |
72 | *) exit 1 ;; | 72 | *) exit 1 ;; |
73 | esac | 73 | esac |
74 | done | 74 | done |
75 | ## Log configuration variables | 75 | ## Log configuration variables |
76 | log config "domain: $DOMAIN" | 76 | log config "domain: $DOMAIN" |
77 | log config "workdir: $WORKD" | 77 | log config "workdir: $WORKD" |
78 | log config "output: $OUTD" | 78 | log config "output: $OUTD" |
79 | ## Initialize state | 79 | ## Initialize state |
80 | FILE= | 80 | FILE= |
81 | ## Cleanup after we're done | 81 | ## Cleanup after we're done |
82 | trap cleanup INT QUIT | 82 | trap cleanup INT QUIT |
83 | } | 83 | } |
84 | 84 | ||
85 | main() { | 85 | main() { |
86 | # State predicates | 86 | # State predicates |
87 | alias pagep=false indexp=false feedp=false | 87 | alias pagep=false indexp=false feedp=false |
88 | # Convenience aliases | 88 | # Convenience aliases |
89 | alias body=cat title='meta title' pubdate='meta date' | 89 | alias body=cat title='meta title' pubdate='meta date' |
90 | # Configure | 90 | # Configure |
91 | configure "$@" | 91 | configure "$@" |
92 | shift "$((OPTIND - 1))" | 92 | shift "$((OPTIND - 1))" |
93 | # Further argument processing --- pre-build | 93 | # Further argument processing --- pre-build |
94 | preprocess "$@" | 94 | preprocess "$@" |
95 | # Prepare | 95 | # Prepare |
96 | cd "$WORKD" || exit 2 | 96 | cd "$WORKD" || exit 2 |
97 | if test -f "$CONFIG"; then | 97 | if test -f "$CONFIG"; then |
98 | # Source ./.vienna.sh, if it exists. | 98 | # Source ./.vienna.sh, if it exists. |
99 | . "$CONFIG" | 99 | . "$CONFIG" |
100 | elif test -n "$CONFIG_ARG"; then | 100 | elif test -n "$CONFIG_ARG"; then |
101 | # If a -c option was passed on the command line but the file | 101 | # If a -c option was passed on the command line but the file |
102 | # doesn't exist, that's an error. If we're just looking for the | 102 | # doesn't exist, that's an error. If we're just looking for the |
103 | # default file, however, there is no error---the user might want | 103 | # default file, however, there is no error---the user might want |
104 | # to use the default configuration. | 104 | # to use the default configuration. |
105 | log "Can't find configuration \`$CONFIG'." | 105 | log "Can't find configuration \`$CONFIG'." |
106 | exit 2 | 106 | exit 2 |
107 | fi | 107 | fi |
108 | mkdir -p "$OUTD" || exit 2 | 108 | mkdir -p "$OUTD" || exit 2 |
109 | mkdir -p "$TMPD" || exit 2 | 109 | mkdir -p "$TMPD" || exit 2 |
110 | # Build pages | 110 | # Build pages |
111 | alias pagep=true | 111 | alias pagep=true |
112 | genpage *."$PAGE_RAW_EXT" || exit 2 | 112 | genpage *."$PAGE_RAW_EXT" || exit 2 |
113 | alias pagep=false | 113 | alias pagep=false |
114 | # Build index | 114 | # Build index |
115 | alias indexp=true | 115 | alias indexp=true |
116 | genlist index_item "$INDEX_TEMPLATE" *."$PAGE_RAW_EXT" >"$OUTD/index.html" || exit 2 | 116 | genlist index_item "$INDEX_TEMPLATE" *."$PAGE_RAW_EXT" >"$OUTD/index.html" || exit 2 |
117 | alias indexp=false | 117 | alias indexp=false |
118 | # Build feed | 118 | # Build feed |
119 | alias feedp=true | 119 | alias feedp=true |
120 | genlist feed_item "$FEED_TEMPLATE" *."$PAGE_RAW_EXT" >"$OUTD/feed.xml" || exit 2 | 120 | genlist feed_item "$FEED_TEMPLATE" *."$PAGE_RAW_EXT" >"$OUTD/feed.xml" || exit 2 |
121 | alias feedp=false | 121 | alias feedp=false |
122 | # Copy static files | 122 | # Copy static files |
123 | static * || exit 2 | 123 | static * || exit 2 |
124 | # Further argument processing --- post-build | 124 | # Further argument processing --- post-build |
125 | postprocess "$@" | 125 | postprocess "$@" |
126 | } | 126 | } |
127 | 127 | ||
128 | preprocess() { | 128 | preprocess() { |
129 | case "${1:-ok}" in | 129 | case "${1:-ok}" in |
130 | ok) ;; | 130 | ok) ;; |
131 | clean) | 131 | clean) |
132 | log vienna "clean" | 132 | log vienna "clean" |
133 | rm -r "$OUTD" | 133 | rm -r "$OUTD" |
134 | exit | 134 | cleanup |
135 | ;; | 135 | exit |
136 | esac | 136 | ;; |
137 | esac | ||
137 | } | 138 | } |
138 | 139 | ||
139 | postprocess() { | 140 | postprocess() { |
140 | case "${1:-ok}" in | 141 | case "${1:-ok}" in |
141 | ok) ;; | 142 | ok) ;; |
142 | publish) | 143 | publish) |
143 | log vienna "publish" | 144 | log vienna "publish" |
144 | publish "$OUTD" | 145 | publish "$OUTD" |
145 | ;; | 146 | ;; |
146 | preview) | 147 | preview) |
147 | log vienna "preview" | 148 | log vienna "preview" |
148 | preview "$OUTD" | 149 | preview "$OUTD" |
149 | ;; | 150 | ;; |
150 | *) | 151 | *) |
151 | log vienna "Don't know command \`$1'." | 152 | log vienna "Don't know command \`$1'." |
152 | exit 1 | 153 | exit 1 |
153 | ;; | 154 | ;; |
154 | esac | 155 | esac |
155 | } | 156 | } |
156 | 157 | ||
157 | cleanup() { | 158 | cleanup() { |
158 | test -z "$DEBUG" && | 159 | test -z "$DEBUG" && |
159 | test -z "$NODEP_RM" && | 160 | test -z "$NODEP_RM" && |
160 | rm -r "$TMPD" | 161 | rm -r "$TMPD" |
161 | } | 162 | } |
162 | 163 | ||
163 | publish() { | 164 | publish() { |
164 | cat <<EOF >&2 | 165 | cat <<EOF >&2 |
165 | 166 | ||
166 | I want to publish your website but I don't know how. | 167 | I want to publish your website but I don't know how. |
167 | Make a $CONFIG file in this directory and write a \`publish' | 168 | Make a $CONFIG file in this directory and write a \`publish' |
@@ -169,11 +170,11 @@ function telling me what to do. I recommend using \`rsync', | |||
169 | but you live your life." | 170 | but you live your life." |
170 | 171 | ||
171 | EOF | 172 | EOF |
172 | exit 3 | 173 | exit 3 |
173 | } | 174 | } |
174 | 175 | ||
175 | preview() { | 176 | preview() { |
176 | cat <<EOF >&2 | 177 | cat <<EOF >&2 |
177 | 178 | ||
178 | I want to show you a preview of your website but I don't | 179 | I want to show you a preview of your website but I don't |
179 | know how. Make a $CONFIG file in this directory and write | 180 | know how. Make a $CONFIG file in this directory and write |
@@ -182,17 +183,17 @@ using something like \`python -m http.server', but you live | |||
182 | your life." | 183 | your life." |
183 | 184 | ||
184 | EOF | 185 | EOF |
185 | exit 3 | 186 | exit 3 |
186 | } | 187 | } |
187 | 188 | ||
188 | ### Utility | 189 | ### Utility |
189 | 190 | ||
190 | log() { | 191 | log() { |
191 | if "$LOG"; then | 192 | if "$LOG"; then |
192 | t="$1" | 193 | t="$1" |
193 | shift | 194 | shift |
194 | echo >&2 "[$t]" "$@" | 195 | echo >&2 "[$t]" "$@" |
195 | fi | 196 | fi |
196 | } | 197 | } |
197 | 198 | ||
198 | ### File processing | 199 | ### File processing |
@@ -200,140 +201,141 @@ log() { | |||
200 | ## Building block functions | 201 | ## Building block functions |
201 | 202 | ||
202 | shellfix() { # shellfix FILE... | 203 | shellfix() { # shellfix FILE... |
203 | ## Replace ` with \`, $ with \$, and $$ with $ | 204 | ## Replace ` with \`, $ with \$, and $$ with $ |
204 | # shellcheck disable=2016 | 205 | # shellcheck disable=2016 |
205 | sed -E \ | 206 | sed -E \ |
206 | -e 's/`/\\`/g' \ | 207 | -e 's/`/\\`/g' \ |
207 | -e 's/(^|[^\$])\$([^\$]|$)/\1\\$\2/g' \ | 208 | -e 's/(^|[^\$])\$([^\$]|$)/\1\\$\2/g' \ |
208 | -e 's/\$\$/$/g' \ | 209 | -e 's/\$\$/$/g' \ |
209 | "$@" | 210 | "$@" |
210 | } | 211 | } |
211 | 212 | ||
212 | expand() { # expand TEMPLATE... < INPUT | 213 | expand() { # expand TEMPLATE... < INPUT |
213 | ## Print TEMPLATE to stdout, expanding shell constructs. | 214 | ## Print TEMPLATE to stdout, expanding shell constructs. |
214 | end="expand_:_${count:=0}_:_end" | 215 | end="expand_:_${count:=0}_:_end" |
215 | eval "$( | 216 | eval "$( |
216 | echo "cat<<$end" | 217 | echo "cat<<$end" |
217 | shellfix "$@" | 218 | shellfix "$@" |
218 | echo | 219 | echo |
219 | echo "$end" | 220 | echo "$end" |
220 | )" && count=$((count + 1)) | 221 | )" && count=$((count + 1)) |
221 | } | 222 | } |
222 | 223 | ||
223 | phtml() { # phtml < INPUT | 224 | phtml() { # phtml < INPUT |
224 | ## Output HTML, pretty much. | 225 | ## Output HTML, pretty much. |
225 | # Paragraphs unadorned with html tags will be wrapped in <p> tags, and | 226 | # Paragraphs unadorned with html tags will be wrapped in <p> tags, and |
226 | # &, <, > will be escaped unless prepended with \. Paragraphs where the | 227 | # &, <, > will be escaped unless prepended with \. Paragraphs where the |
227 | # first character is < will be left as-is, excepting indentation on the | 228 | # first character is < will be left as-is, excepting indentation on the |
228 | # first line (an implementation detail). | 229 | # first line (an implementation detail). |
229 | sed -E \ | 230 | sed -E \ |
230 | '/./{H;1h;$!d;}; x; | 231 | '/./{H;1h;$!d;}; x; |
231 | s#^[ \n\t]+[^<].*#&#; | 232 | s#^[ \n\t]+[^<].*#&#; |
232 | t par; b; | 233 | t par; b; |
233 | :par; | 234 | :par; |
234 | s#([^\\])&#\1\&#g; s#\\&#\&#g; | 235 | s#([^\\])&#\1\&#g; s#\\&#\&#g; |
235 | s#([^\\])<#\1\<#g; s#\\<#<#g; | 236 | s#([^\\])<#\1\<#g; s#\\<#<#g; |
236 | s#([^\\])>#\1\>#g; s#\\>#>#g;' | 237 | s#([^\\])>#\1\>#g; s#\\>#>#g;' |
237 | } | 238 | } |
238 | 239 | ||
239 | meta() { # meta FIELD [FILE] < INPUT | 240 | meta() { # meta FIELD [FILE] < INPUT |
240 | ## Extract metadata FIELDS from INPUT. | 241 | ## Extract metadata FIELDS from INPUT. |
241 | # FILE gives the filename to save metadata to in the $WORKD. It | 242 | # FILE gives the filename to save metadata to in the $WORKD. It |
242 | # defaults to the current value for $FILE. | 243 | # defaults to the current value for $FILE. |
243 | # | 244 | # |
244 | # Metadata should exist as colon-separated data in an HTML comment at | 245 | # Metadata should exist as colon-separated data in an HTML comment at |
245 | # the beginning of an input file. | 246 | # the beginning of an input file. |
246 | field="$1" | 247 | field="$1" |
247 | file="${2:-$FILE}" | 248 | file="${2:-$FILE}" |
248 | metafile="$TMPD/${file}.meta" | 249 | metafile="${TMPD:=.}/${file}.meta" |
249 | test -f "$metafile" || | 250 | test -f "$metafile" || |
250 | sed '/<!--/n;/-->/q' >"$metafile" | 251 | sed '/<!--/!q;/<!--/n;/-->/q' >"$metafile" |
251 | sed -n "s/^[ \t]*$field:[ \t]*//p" <"$metafile" | 252 | sed -n "s/^[ \t]*$field:[ \t]*//p" <"$metafile" |
252 | } | 253 | } |
253 | 254 | ||
254 | ## Customizable bits | 255 | ## Customizable bits |
255 | 256 | ||
256 | filters() { # filters < INPUT | 257 | filters() { # filters < INPUT |
257 | ## The filters to run input through. | 258 | ## The filters to run input through. |
258 | # This is a good candidate for customization in .vienna.sh. | 259 | # This is a good candidate for customization in .vienna.sh. |
259 | expand | phtml | 260 | expand | phtml |
260 | } | 261 | } |
261 | 262 | ||
262 | ### Site building | 263 | ### Site building |
263 | 264 | ||
264 | genpage() { # genpage PAGE... | 265 | genpage() { # genpage PAGE... |
265 | ## Compile PAGE(s) into $OUTD for publication. | 266 | ## Compile PAGE(s) into $OUTD for publication. |
266 | # Outputs a file of the format $OUTD/<PAGE>/index.html. | 267 | # Outputs a file of the format $OUTD/<PAGE>/index.html. |
267 | test -f "$PAGE_TEMPLATE" || return 1 | 268 | test -f "$PAGE_TEMPLATE" || return 1 |
268 | for FILE; do | 269 | for FILE; do |
269 | log genpage "$FILE" | 270 | log genpage "$FILE" |
270 | outd="$OUTD/${FILE%.$PAGE_RAW_EXT}" | 271 | outd="$OUTD/${FILE%.$PAGE_RAW_EXT}" |
271 | outf="$outd/index.html" | 272 | outf="$outd/index.html" |
272 | tmpf="$TMPD/$FILE.tmp" | 273 | tmpf="$TMPD/$FILE.tmp" |
273 | mkdir -p "$outd" | 274 | mkdir -p "$outd" |
274 | filters <"$FILE" >"$tmpf" | 275 | filters <"$FILE" >"$tmpf" |
275 | expand "$PAGE_TEMPLATE" <"$tmpf" >"$outf" | 276 | expand "$PAGE_TEMPLATE" <"$tmpf" >"$outf" |
276 | done | 277 | done |
277 | } | 278 | } |
278 | 279 | ||
279 | genlist() { # genlist PERITEM_FUNC TEMPLATE_FILE PAGE... | 280 | genlist() { # genlist PERITEM_FUNC TEMPLATE_FILE PAGE... |
280 | peritem_func="$1" | 281 | peritem_func="$1" |
281 | template_file="$2" | 282 | template_file="$2" |
282 | shift 2 || return 2 | 283 | shift 2 || return 2 |
283 | test -f "$template_file" || return 1 | 284 | test -f "$template_file" || return 1 |
284 | for FILE; do | 285 | for FILE; do |
285 | log genlist "$peritem_func/$template_file: $FILE" | 286 | log genlist "$peritem_func: $template_file: $FILE" |
286 | LINK="$DOMAIN${DOMAIN:+/}${1%.PAGE_RAW_EXT}" | 287 | |
287 | done | expand "$template_file" | 288 | LINK="$DOMAIN${DOMAIN:+/}${1%.PAGE_RAW_EXT}" |
289 | done | expand "$template_file" | ||
288 | } | 290 | } |
289 | 291 | ||
290 | index_item() { # index_item PAGE | 292 | index_item() { # index_item PAGE |
291 | ## Construct a single item in an index.html. | 293 | ## Construct a single item in an index.html. |
292 | echo "<li><a href=\"$LINK\">$(meta title "$1")</a></li>" | 294 | echo "<li><a href=\"$LINK\">$(meta title "$1")</a></li>" |
293 | } | 295 | } |
294 | 296 | ||
295 | feed_item() { # feed_item PAGE | 297 | feed_item() { # feed_item PAGE |
296 | ## Construct a single item in an RSS feed. | 298 | ## Construct a single item in an RSS feed. |
297 | date="$(pubdate "$1")" | 299 | date="$(pubdate "$1")" |
298 | echo "<item>" | 300 | echo "<item>" |
299 | echo "<title>$(meta title "$1")</title>" | 301 | echo "<title>$(meta title "$1")</title>" |
300 | echo "<link>$LINK</link>" | 302 | echo "<link>$LINK</link>" |
301 | echo "<guid>$LINK</guid>" | 303 | echo "<guid>$LINK</guid>" |
302 | test -n "$date" && echo "<pubDate>$date</pubDate>" | 304 | test -n "$date" && echo "<pubDate>$date</pubDate>" |
303 | echo "</item>" | 305 | echo "</item>" |
304 | } | 306 | } |
305 | 307 | ||
306 | index() { # index PAGE... | 308 | index() { # index PAGE... |
307 | ## Build a site index from all PAGE(s) passed to it. | 309 | ## Build a site index from all PAGE(s) passed to it. |
308 | # Wraps each PAGE in a <li><a> structure. | 310 | # Wraps each PAGE in a <li><a> structure. |
309 | test -f "$INDEX_TEMPLATE" || return 1 | 311 | test -f "$INDEX_TEMPLATE" || return 1 |
310 | for FILE; do | 312 | for FILE; do |
311 | log index "$FILE" | 313 | log index "$FILE" |
312 | index_item "$FILE" | 314 | index_item "$FILE" |
313 | done | expand "$INDEX_TEMPLATE" >"$OUTD/index.html" | 315 | done | expand "$INDEX_TEMPLATE" >"$OUTD/index.html" |
314 | } | 316 | } |
315 | 317 | ||
316 | feed() { # feed PAGE... | 318 | feed() { # feed PAGE... |
317 | ## Build an RSS 2.0 feed from PAGE(s). | 319 | ## Build an RSS 2.0 feed from PAGE(s). |
318 | test -f "$FEED_TEMPLATE" || return 1 | 320 | test -f "$FEED_TEMPLATE" || return 1 |
319 | for FILE; do | 321 | for FILE; do |
320 | log feed "$FILE" | 322 | log feed "$FILE" |
321 | feed_item "$FILE" | 323 | feed_item "$FILE" |
322 | done | expand "$FEED_TEMPLATE" >"$OUTD/feed.xml" | 324 | done | expand "$FEED_TEMPLATE" >"$OUTD/feed.xml" |
323 | } | 325 | } |
324 | 326 | ||
325 | static() { # static FILE... | 327 | static() { # static FILE... |
326 | ## Copy static FILE(s) to $OUTD as-is. | 328 | ## Copy static FILE(s) to $OUTD as-is. |
327 | # Performs a simple heuristic to determine whether to copy a file or | 329 | # Performs a simple heuristic to determine whether to copy a file or |
328 | # not. | 330 | # not. |
329 | for FILE; do | 331 | for FILE; do |
330 | case "$FILE" in | 332 | case "$FILE" in |
331 | .*) continue ;; | 333 | .*) continue ;; |
332 | *.htm) continue ;; | 334 | *.htm) continue ;; |
333 | "$OUTD") continue ;; | 335 | "$OUTD") continue ;; |
334 | *) cp -r "$FILE" "$OUTD/" ;; | 336 | *) cp -r "$FILE" "$OUTD/" ;; |
335 | esac | 337 | esac |
336 | done | 338 | done |
337 | } | 339 | } |
338 | 340 | ||
339 | ### Do the thing! | 341 | ### Do the thing! |