diff options
-rwxr-xr-x | vienna | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/vienna b/vienna new file mode 100755 index 0000000..f7daabc --- /dev/null +++ b/vienna | |||
@@ -0,0 +1,210 @@ | |||
1 | #!/bin/sh | ||
2 | # vienna --- a tiny, tasty ssg | ||
3 | # by C. Duckworth <acdw@acdw.net> | ||
4 | |||
5 | ### Entry point | ||
6 | |||
7 | configure() { # configure ARG... | ||
8 | ## Set up environment | ||
9 | DOMAIN="${VIENNA_DOMAIN:-https://www.example.com}" | ||
10 | TMPD="${VIENNA_TMPD:-/tmp/vienna}" | ||
11 | WORKD="${VIENNA_WORKD:-$PWD}" | ||
12 | OUTD="${VIENNA_OUTD:-out}" | ||
13 | CONFIG="${VIENNA_CONFIG:-./.vienna.sh}" | ||
14 | # Templates | ||
15 | PAGE_TEMPLATE="${VIENNA_PAGE_TEMPLATE:-.page.tmpl.html}" | ||
16 | INDEX_TEMPLATE="${VIENNA_INDEX_TEMPLATE:-.index.tmpl.html}" | ||
17 | FEED_TEMPLATE="${VIENNA_FEED_TEMPLATE:-.feed.tmpl.xml}" | ||
18 | # File extensions | ||
19 | PAGE_RAW_EXT="${VIENNA_PAGE_RAW_EXT:-htm}" | ||
20 | ## Source ./.vienna.sh, if it exists. | ||
21 | # Sourcing it here lets it override any environment variables, and it to | ||
22 | # be overridden by command-line flags. | ||
23 | test -f "$CONFIG" && . "$CONFIG" | ||
24 | ## Parse command line arguments | ||
25 | while getopts d:C:o: opt; do | ||
26 | case "$opt" in | ||
27 | C) WORKD="$OPTARG" ;; | ||
28 | d) DOMAIN="$OPTARG" ;; | ||
29 | o) OUTD="$OPTARG" ;; | ||
30 | *) exit 1 ;; | ||
31 | esac | ||
32 | done | ||
33 | ## Initialize state | ||
34 | FILE= | ||
35 | ## Cleanup after we're done | ||
36 | trap cleanup INT KILL | ||
37 | } | ||
38 | |||
39 | main() { | ||
40 | # State predicates | ||
41 | alias pagep=false indexp=false feedp=false | ||
42 | # Convenience aliases | ||
43 | alias body=cat title='meta title' pubdate='meta date' | ||
44 | # Configure | ||
45 | configure "$@" | ||
46 | shift "$((OPTIND - 1))" | ||
47 | # Prepare | ||
48 | cd "$WORKD" || exit 2 | ||
49 | mkdir -p "$OUTD" || exit 2 | ||
50 | mkdir -p "$TMPD" || exit 2 | ||
51 | # Build pages | ||
52 | alias pagep=true | ||
53 | build *."$PAGE_RAW_EXT" || exit 2 | ||
54 | alias pagep=false | ||
55 | # Build index | ||
56 | alias indexp=true | ||
57 | index *."$PAGE_RAW_EXT" || exit 2 | ||
58 | alias indexp=false | ||
59 | # Build feed | ||
60 | alias feedp=true | ||
61 | feed *."$PAGE_RAW_EXT" || exit 2 | ||
62 | alias feedp=false | ||
63 | # Copy static files | ||
64 | static * || exit 2 | ||
65 | # If $1 is 'publish', yeet the out/ directory somewhere | ||
66 | if test "x$1" = xpublish; then | ||
67 | publish "$OUTD" | ||
68 | fi | ||
69 | } | ||
70 | |||
71 | cleanup() { | ||
72 | test -z "$DEBUG" && | ||
73 | test -z "$NODEP_RM" && | ||
74 | rm -r "$TMPD" | ||
75 | } | ||
76 | |||
77 | publish() { | ||
78 | echo >&2 "I want to publish your website but I don't know how." | ||
79 | echo >&2 "Make a $CONFIG file in this directory and write a" | ||
80 | echo >&2 "\`publish' function telling me what to do. I rec-" | ||
81 | echo >&2 "ommend using \`rsync', but you live your life." | ||
82 | exit 3 | ||
83 | } | ||
84 | |||
85 | ### File processing | ||
86 | |||
87 | ## Building block functions | ||
88 | |||
89 | shellfix() { # shellfix FILE... | ||
90 | ## Replace ` with \`, $ with \$, and $$ with $ | ||
91 | sed -E \ | ||
92 | -e 's/`/\\`/g' \ | ||
93 | -e 's/(^|[^\$])\$([^\$]|$)/\1\\$\2/g' \ | ||
94 | -e 's/\$\$/$/g' \ | ||
95 | "$@" | ||
96 | } | ||
97 | |||
98 | expand() { # expand TEMPLATE... < INPUT | ||
99 | ## Print TEMPLATE to stdout, expanding shell constructs. | ||
100 | end="expand_:_${count:=0}_:_end" | ||
101 | eval "$( | ||
102 | echo "cat<<$end" | ||
103 | shellfix "$@" | ||
104 | echo | ||
105 | echo "$end" | ||
106 | )" && count=$((count + 1)) | ||
107 | } | ||
108 | |||
109 | phtml() { # phtml < INPUT | ||
110 | ## Output HTML, pretty much. | ||
111 | # Paragraphs unadorned with html tags will be wrapped in <p> tags, and | ||
112 | # &, <, > will be escaped unless prepended with \. Paragraphs where the | ||
113 | # first character is < will be left as-is, excepting indentation on the | ||
114 | # first line (an implementation detail). | ||
115 | sed -E \ | ||
116 | '/./{H;1h;$!d;}; x; | ||
117 | s#^[ \n\t]\+##; | ||
118 | t ok; :ok; | ||
119 | s#^[^<].*#&#; | ||
120 | t par; b; | ||
121 | :par; | ||
122 | s#([^\\])&#\1\&#g; s#\\&#\&#g; | ||
123 | s#([^\\])<#\1\<#g; s#\\<#<#g; | ||
124 | s#([^\\])>#\1\>#g; s#\\>#>#g;' | ||
125 | } | ||
126 | |||
127 | meta() { # meta FIELD [FILE] < INPUT | ||
128 | ## Extract metadata FIELDS from INPUT. | ||
129 | # FILE gives the filename to save metadata to in the $WORKD. It | ||
130 | # defaults to the current value for $FILE. | ||
131 | # | ||
132 | # Metadata should exist as colon-separated data in an HTML comment at | ||
133 | # the beginning of an input file. | ||
134 | field="$1" | ||
135 | file="${2:-$FILE}" | ||
136 | metafile="$TMPD/${file}.meta" | ||
137 | test -f "$metafile" || | ||
138 | sed '/<!--/n;/-->/q' >"$metafile" | ||
139 | sed -n "s/^[ \t]*$1:[ \t]*//p" <"$metafile" | ||
140 | } | ||
141 | |||
142 | ## Customizable bits | ||
143 | |||
144 | filters() { # filters < INPUT | ||
145 | ## The filters to run input through. | ||
146 | # This is a good candidate for customization in .vienna.sh. | ||
147 | expand | phtml | ||
148 | } | ||
149 | |||
150 | ### Site building | ||
151 | |||
152 | build() { # build PAGE... | ||
153 | ## Compile PAGE(s) into $OUTD for publication. | ||
154 | # Outputs a file of the format $OUTD/<PAGE>/index.html. | ||
155 | test -f "$PAGE_TEMPLATE" || return 1 | ||
156 | for FILE; do | ||
157 | echo >&2 "[build] $FILE" | ||
158 | outd="$OUTD/${FILE%.$PAGE_RAW_EXT}" | ||
159 | outf="$outd/index.html" | ||
160 | tmpf="$TMPD/$FILE.tmp" | ||
161 | mkdir -p "$outd" | ||
162 | filters <"$FILE" >"$tmpf" | ||
163 | expand "$PAGE_TEMPLATE" <"$tmpf" >"$outf" | ||
164 | done | ||
165 | } | ||
166 | |||
167 | index() { # index PAGE... | ||
168 | ## Build a site index from all PAGE(s) passed to it. | ||
169 | # Wraps each PAGE in a <li><a> structure. | ||
170 | test -f "$INDEX_TEMPLATE" || return 1 | ||
171 | for FILE; do | ||
172 | echo >&2 "[index] $FILE" | ||
173 | link="$DOMAIN${DOMAIN:+/}${FILE%.$PAGE_RAW_EXT}" | ||
174 | echo "<li><a href=\"$link\">$(meta title "$FILE")</a></li>" | ||
175 | done | expand "$INDEX_TEMPLATE" >"$OUTD/index.html" | ||
176 | } | ||
177 | |||
178 | feed() { # feed PAGE... | ||
179 | ## Build an RSS 2.0 feed from PAGE(s). | ||
180 | test -f "$FEED_TEMPLATE" || return 1 | ||
181 | for FILE; do | ||
182 | echo >&2 "[feed] $FILE" | ||
183 | link="$DOMAIN${DOMAIN:+/}${FILE%.$PAGE_RAW_EXT}" | ||
184 | date="$(meta pubdate "$FILE")" | ||
185 | echo "<item>" | ||
186 | echo "<title>$(meta title "$FILE")</title>" | ||
187 | echo "<link>$link</link>" | ||
188 | echo "<guid>$link</guid>" | ||
189 | test -n "$date" && echo "<pubDate>$date</pubDate>" | ||
190 | echo "</item>" | ||
191 | done | expand "$FEED_TEMPLATE" >"$OUTD/feed.xml" | ||
192 | } | ||
193 | |||
194 | static() { # static FILE... | ||
195 | ## Copy static FILE(s) to $OUTD as-is. | ||
196 | # Performs a simple heuristic to determine whether to copy a file or | ||
197 | # not. | ||
198 | for FILE; do | ||
199 | case "$FILE" in | ||
200 | .*) continue ;; | ||
201 | "$OUTD") continue ;; | ||
202 | *) cp -r "$FILE" "$OUTD/" ;; | ||
203 | esac | ||
204 | done | ||
205 | } | ||
206 | |||
207 | ### Do the thing! | ||
208 | |||
209 | test -n "$DEBUG" && set -x | ||
210 | test -n "$SOURCE" || main "$@" | ||