diff options
-rw-r--r-- | feed.tmpl.xml | 12 | ||||
-rw-r--r-- | footer.htm | 1 | ||||
-rw-r--r-- | header.htm | 2 | ||||
-rwxr-xr-x | ht | 287 | ||||
-rwxr-xr-x | ht.sh | 60 | ||||
-rw-r--r-- | index.tmpl.htm | 12 | ||||
-rw-r--r-- | page.tmpl.htm | 10 | ||||
-rw-r--r-- | test.ht | 32 |
8 files changed, 321 insertions, 95 deletions
diff --git a/feed.tmpl.xml b/feed.tmpl.xml new file mode 100644 index 0000000..13182b3 --- /dev/null +++ b/feed.tmpl.xml | |||
@@ -0,0 +1,12 @@ | |||
1 | <?xml version="1.0" encoding="utf-8"?> | ||
2 | <feed xmlns="http://www.w3.org/2005/Atom"> | ||
3 | <title>acdw</title> | ||
4 | <subtitle>a home on the web</subtitle> | ||
5 | <link href="$BASEURL/$FEEDNAME" rel="self" /> | ||
6 | <link href="$BASEURL" /> | ||
7 | <id>$BASEURL</id> | ||
8 | <generator uri="https://git.acdw.net/ht" version="infinite">ht</generator> | ||
9 | <rights>https://acdw.casa/gcl/</rights> | ||
10 | <updated>$(date -R)</updated> | ||
11 | $(cat) | ||
12 | </feed> | ||
diff --git a/footer.htm b/footer.htm deleted file mode 100644 index 0ceac0f..0000000 --- a/footer.htm +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | <!-- so ends ${file} --> | ||
diff --git a/header.htm b/header.htm deleted file mode 100644 index 14eeb22..0000000 --- a/header.htm +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <title>${title}</title> | ||
diff --git a/ht b/ht new file mode 100755 index 0000000..c8cef5c --- /dev/null +++ b/ht | |||
@@ -0,0 +1,287 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | ### Code: | ||
4 | |||
5 | usage() { | ||
6 | cat <<\EOF | ||
7 | ht: Build a website | ||
8 | Usage: ht -h | ||
9 | ht [OPTIONS...] FILE... | ||
10 | |||
11 | FLAGS: | ||
12 | -h Show this help and exit. | ||
13 | -B Treat all targets as "new" (like in `make'). | ||
14 | |||
15 | OPTIONS: | ||
16 | -u BASEURL The base URL of the built site. Default: https://example.com | ||
17 | -o OUTD Output built files to OUTD. Default: ./out/ | ||
18 | -w WORKD Use WORKD as the working directory. Default: /tmp/ht/ | ||
19 | -p PROC Use PROC to process each input file. Default: ./ht.awk | ||
20 | -P TEMPLATE Use TEMPLATE as the page template. Default: ./page.tmpl.htm | ||
21 | -i ITEMFMT Format individual items with ITEMFMT in the site index. | ||
22 | -I TEMPLATE Use TEMPLATE as the index template. Default: ./index.tmpl.htm | ||
23 | -f ITEMFMT Format individual items with ITEMFMT in the site feed. | ||
24 | -F TEMPLATE Use TEMPLATE as the feed template. Default: ./feed.tmpl.xml | ||
25 | -s DIRECTORY The DIRECTORY holding static files (images, css, etc.). | ||
26 | Default: ./static/ | ||
27 | -S DIRECTORY The DIRECTORY to copy static files to. Default: ./out/static. | ||
28 | |||
29 | PARAMETERS: | ||
30 | FILE... The list of files to include in the website. | ||
31 | EOF | ||
32 | exit "${1:-0}" | ||
33 | } | ||
34 | |||
35 | main() { | ||
36 | configure "$@" | ||
37 | shift $((OPTIND - 1)) | ||
38 | prepare | ||
39 | static_copy | ||
40 | |||
41 | for input; do | ||
42 | page_write "$PAGE_TEMPLATE" "$input" | ||
43 | done | ||
44 | |||
45 | if [ -n "$INDEXEACH" ]; then | ||
46 | index_write "$INDEX_TEMPLATE" "$INDEXEACH" "$OUTD/$INDEXNAME" | ||
47 | fi | ||
48 | if [ -n "$FEEDEACH" ]; then | ||
49 | index_write "$FEED_TEMPLATE" "$FEEDEACH" "$OUTD/$FEEDNAME" | ||
50 | fi | ||
51 | printf 'Done. ' >&2 | ||
52 | if test -f "$WORKD/ok"; then | ||
53 | eprint "No errors reported." | ||
54 | else | ||
55 | eprint "There were errors." | ||
56 | fi | ||
57 | } | ||
58 | |||
59 | configure() { | ||
60 | OUTD="${HT_OUT_DIR:-./out}" | ||
61 | WORKD="${HT_WORKD:-/tmp/ht}" | ||
62 | PROC="${HT_PROC:-./ht.awk}" # XXX: Needs better resolution | ||
63 | BASEURL="${HT_BASEURL:-https://example.com}" | ||
64 | PAGE_TEMPLATE="${HT_PAGE_TEMPLATE:-./page.tmpl.htm}" | ||
65 | INDEX_TEMPLATE="${HT_INDEX_TEMPLATE:-./index.tmpl.htm}" | ||
66 | FEED_TEMPLATE="${HT_FEED_TEMPLATE:-./feed.tmpl.xml}" | ||
67 | ALLNEW=false | ||
68 | STATICD="${HT_STATIC_DIR:-./static}" | ||
69 | STATICOUT="${HT_STATIC_OUTPUT_DIR:-${OUTD}/static}" | ||
70 | |||
71 | INDEXNAME="${HT_INDEX_NAME:-index.html}" | ||
72 | if [ -n "$HT_INDEX_ITEM_FORMAT" ]; then | ||
73 | INDEXEACH="$HT_INDEX_ITEM_FORMAT" | ||
74 | else | ||
75 | # shellcheck disable=2016 | ||
76 | INDEXEACH='<li><a href=\"$BASEURL/${file}/\">${title}</a></li>' | ||
77 | fi | ||
78 | |||
79 | FEEDNAME="${HT_FEED_NAME:-feed.xml}" | ||
80 | if [ -n "$HT_FEED_ITEM_FORMAT" ]; then | ||
81 | FEEDEACH="$HT_FEED_ITEM_FORMAT" | ||
82 | else | ||
83 | # shellcheck disable=2016 | ||
84 | FEEDEACH='<entry> | ||
85 | <id>$BASEURL/$file</id> | ||
86 | <link rel=\"alternate\" href=\"$BASEURL/$file/\" /> | ||
87 | <title>${title}</title> | ||
88 | <updated>$(date -u -d "@${time}" -R)</updated> | ||
89 | <author>$AUTHOR</author> | ||
90 | <content type=\"text/html\"> | ||
91 | <![CDATA[$(cat "$WORKD/${file}.body")]]> | ||
92 | </content> | ||
93 | </entry>' | ||
94 | fi | ||
95 | |||
96 | # shellcheck disable=2034 # BASEURL is used in templates | ||
97 | while getopts hBo:w:p:i:I:f:F:u:s:S: opt; do | ||
98 | case "$opt" in | ||
99 | h) usage ;; | ||
100 | o) OUTD="$OPTARG" ;; | ||
101 | w) WORKD="$OPTARG" ;; | ||
102 | p) PROC="$OPTARG" ;; | ||
103 | P) PAGE_TEMPLATE="$OPTARG" ;; | ||
104 | i) INDEXEACH="$OPTARG" ;; | ||
105 | I) INDEX_TEMPLATE="$OPTARG" ;; | ||
106 | f) FEEDEACH="$OPTARG" ;; | ||
107 | F) FEED_TEMPLATE="$OPTARG" ;; | ||
108 | u) BASEURL="$OPTARG" ;; | ||
109 | B) ALLNEW=true ;; | ||
110 | s) STATICD="$OPTARG" ;; | ||
111 | S) STATICOUT="$OPTARG" ;; | ||
112 | *) usage 1 ;; | ||
113 | esac | ||
114 | done | ||
115 | |||
116 | INDEX="$WORKD/index.txt" | ||
117 | } | ||
118 | |||
119 | prepare() { | ||
120 | test -n "$WORKD" && rm -rf "$WORKD" | ||
121 | mkdir -p "$OUTD" "$WORKD" | ||
122 | test -x "$PROC" || { | ||
123 | eprint "Can't find processor: $PROC" | ||
124 | exit 2 | ||
125 | } | ||
126 | touch "$WORKD/ok" | ||
127 | } | ||
128 | |||
129 | ### Utilities | ||
130 | |||
131 | print() { | ||
132 | printf '%s\n' "$*" | ||
133 | } | ||
134 | |||
135 | eprint() { | ||
136 | print "$@" >&2 | ||
137 | } | ||
138 | |||
139 | olderp() { # olderp REF OTHER | ||
140 | # Is REF older than OTHER ? Necessary b/c test -ot is not POSIX. | ||
141 | a="$1" | ||
142 | b="$2" | ||
143 | |||
144 | if a_age="$(stat -c%Y "$a" 2>/dev/null)"; then | ||
145 | a_exist=true | ||
146 | else | ||
147 | a_exist=false | ||
148 | fi | ||
149 | if b_age="$(stat -c%Y "$b" 2>/dev/null)"; then | ||
150 | b_exist=true | ||
151 | else | ||
152 | b_exist=false | ||
153 | fi | ||
154 | |||
155 | if $a_exist && ! $b_exist; then | ||
156 | # B doesn't exist -- so B is newer | ||
157 | # eprint "$a is older than $b (doesn't exist)" | ||
158 | return 0 | ||
159 | elif ! $a_exist && $b_exist; then | ||
160 | # A doesn't exist -- so A is newer | ||
161 | # eprint "$a (doesn't exist) is newer than $b" | ||
162 | return 1 | ||
163 | else | ||
164 | # A's age > B's age ? | ||
165 | # eprint "$a ($a_age) <= $b ($b_age)?" | ||
166 | test "$a_age" -le "$b_age" | ||
167 | fi | ||
168 | } | ||
169 | |||
170 | uptodate() { # uptodate FILE DEPENDENCIES... # && return | ||
171 | # Check if FILE is up-to-date with DEPENDENCIES. | ||
172 | $ALLNEW && return 1 | ||
173 | uptodate=1 | ||
174 | file="$1" | ||
175 | shift | ||
176 | for dep in "$@"; do | ||
177 | olderp "$dep" "$file" && uptodate=0 | ||
178 | done | ||
179 | return $uptodate | ||
180 | } | ||
181 | |||
182 | ### File conversion | ||
183 | |||
184 | template_expand() { # template_expand TEMPLATES... | ||
185 | end="tmpl_$(date +%s)_${count:=0}" | ||
186 | eval "$( | ||
187 | print "cat <<$end" | ||
188 | cat "$@" | ||
189 | |||
190 | print "$end" | ||
191 | )" | ||
192 | count=$((count + 1)) | ||
193 | } | ||
194 | |||
195 | meta_save() { # meta_save [-c COMMENTCH] [-m METACH] META_FILE < INPUT | ||
196 | COMMENTCH=';' | ||
197 | METACH='@' | ||
198 | while getopts c:m: opt; do | ||
199 | case "$opt" in | ||
200 | c) COMMENTCH="$OPTARG" ;; | ||
201 | m) METACH="$OPTARG" ;; | ||
202 | *) ;; | ||
203 | esac | ||
204 | done | ||
205 | shift $((OPTIND - 1)) | ||
206 | sed -n "s/^${COMMENTCH}${COMMENTCH}${METACH}//p" 2>/dev/null | tee "$1" | ||
207 | } | ||
208 | |||
209 | meta_clear() { # meta_clear META_FILE | ||
210 | while read -r line; do | ||
211 | case "$line" in | ||
212 | *'()'*) unset -f "${line%()*}" ;; | ||
213 | *=*) unset -v "${line%=*}" ;; | ||
214 | *) ;; | ||
215 | esac | ||
216 | done <"$1" 2>/dev/null | ||
217 | } | ||
218 | |||
219 | page_write() { # html_write TEMPLATE INPUT | ||
220 | template="$1" | ||
221 | file="$2" | ||
222 | |||
223 | fn="${file##*/}" | ||
224 | fn="${fn%%.*}" | ||
225 | out="${OUTD}/${fn}/index.html" | ||
226 | mkdir -p "${out%/*}" | ||
227 | meta="$WORKD/${fn}.meta" | ||
228 | |||
229 | eval "$(meta_save "$meta" <"$file")" | ||
230 | stat -c "%Y ${fn} $meta" "$file" >>"$INDEX" | ||
231 | |||
232 | uptodate "$out" "$template" "$file" && return | ||
233 | |||
234 | eprint "Page: $file -> $out" | ||
235 | if | ||
236 | ! "$PROC" "$file" | | ||
237 | tee "$WORKD/${fn}.body" | | ||
238 | template_expand "$template" >"$out" | ||
239 | then | ||
240 | eprint "$file -> $out ... ERROR!" | ||
241 | rm "$WORKD/ok" | ||
242 | return | ||
243 | fi | ||
244 | meta_clear "$meta" | ||
245 | } | ||
246 | |||
247 | index_write() { # index_write TEMPLATE EACH OUTFILE | ||
248 | template="$1" | ||
249 | each="$2" | ||
250 | out="$3" | ||
251 | |||
252 | test -f "$INDEX" || return 1 | ||
253 | uptodate "$out" "$template" "$INDEX" && return | ||
254 | |||
255 | eprint "Index: $out" | ||
256 | # shellcheck disable=2034 # file and time can be used in `each' | ||
257 | sort -k1,1 -nr "$INDEX" | | ||
258 | while IFS=' ' read -r time file meta; do | ||
259 | # shellcheck disable=1090 | ||
260 | . "$meta" | ||
261 | if ! item="$(eval print "\"$each\"")"; then | ||
262 | eprint "ERROR: couldn't add '$file' to $out" | ||
263 | rm "$WORKD/ok" | ||
264 | fi | ||
265 | print "$item" | ||
266 | meta_clear "$meta" | ||
267 | done | | ||
268 | template_expand "$template" >"$out" | ||
269 | } | ||
270 | |||
271 | ### Static files | ||
272 | |||
273 | static_copy() { # static_copy | ||
274 | test -d "$STATICD" || return 1 | ||
275 | if command -v rsync 2>/dev/null; then | ||
276 | eprint rsync -avz "$STATICD/" "$STATICOUT/" | ||
277 | rsync -avz "$STATICD/" "$STATICOUT/" | ||
278 | else | ||
279 | eprint cp -r "$STATICD"/* "$STATICOUT/" | ||
280 | cp -r "$STATICD"/* "$STATICOUT/" | ||
281 | fi | ||
282 | } | ||
283 | |||
284 | ### Do the thing | ||
285 | |||
286 | test "$DEBUG" && set -x | ||
287 | test "$SOURCE" || main "$@" | ||
diff --git a/ht.sh b/ht.sh deleted file mode 100755 index 7f20255..0000000 --- a/ht.sh +++ /dev/null | |||
@@ -1,60 +0,0 @@ | |||
1 | #!/bin/sh | ||
2 | # ht.sh | ||
3 | # *.ht -> *html | ||
4 | |||
5 | # config | ||
6 | header_file=header.htm | ||
7 | footer_file=footer.htm | ||
8 | meta_file=meta.sh | ||
9 | |||
10 | print() { | ||
11 | printf '%s\n' "$*" | ||
12 | } | ||
13 | |||
14 | htt() { # htt FILE | ||
15 | # Like `cat`, but with templating. | ||
16 | : "${HT_TMPL_COUNT:=0}" | ||
17 | ht_end="ht_main_$(date +%s)_${HT_TMPL_COUNT}" # be extra double sure | ||
18 | eval "$( | ||
19 | print "cat <<$ht_end" | ||
20 | cat "$@" | ||
21 | |||
22 | print "$ht_end" | ||
23 | )" | ||
24 | HT_TMPL_COUNT=$((HT_TMPL_COUNT + 1)) | ||
25 | } | ||
26 | |||
27 | htmeta_clear() { | ||
28 | # Generate metadata-clearing commands from $meta_file. | ||
29 | while read -r line; do | ||
30 | case "$line" in | ||
31 | *'()'*) # function | ||
32 | unset -f "${line%()*}" | ||
33 | ;; | ||
34 | *=*) # variable assignment | ||
35 | unset -v "${line%=*}" | ||
36 | ;; | ||
37 | *) # other -- XXX: Don't know what to do | ||
38 | ;; | ||
39 | esac | ||
40 | done <"$meta_file" 2>/dev/null | ||
41 | } | ||
42 | |||
43 | htmeta() { # htmeta FILE | ||
44 | # Collect metadata from FILE. | ||
45 | # Metadata looks like this: `;;@<SHELL_EXPRESSION>` | ||
46 | sed -n 's/^;;@//p' "$1" 2>/dev/null | tee "$meta_file" | ||
47 | } | ||
48 | |||
49 | main() { | ||
50 | # Make two passes over each input file, collecting metadata and content. | ||
51 | # Of course, this isn't safe, but you trust yourself, right? | ||
52 | for file; do | ||
53 | eval "$(htmeta_clear)" | ||
54 | eval "$(htmeta "$file")" | ||
55 | ./ht.awk <"$file" | htt "$header_file" - "$footer_file" | ||
56 | done | ||
57 | } | ||
58 | |||
59 | test "$DEBUG" && set -x | ||
60 | test "$SOURCE" || main "$@" | ||
diff --git a/index.tmpl.htm b/index.tmpl.htm new file mode 100644 index 0000000..5884af5 --- /dev/null +++ b/index.tmpl.htm | |||
@@ -0,0 +1,12 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <title>acdw!</title> | ||
5 | </head> | ||
6 | <body> | ||
7 | <ul> | ||
8 | $(cat) | ||
9 | </ul> | ||
10 | <footer>cool B)</footer> | ||
11 | </body> | ||
12 | </html> | ||
diff --git a/page.tmpl.htm b/page.tmpl.htm new file mode 100644 index 0000000..6655494 --- /dev/null +++ b/page.tmpl.htm | |||
@@ -0,0 +1,10 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <title>${title}</title> | ||
5 | </head> | ||
6 | <body> | ||
7 | $(cat) | ||
8 | <!-- so ends ${file} --> | ||
9 | </body> | ||
10 | </html> | ||
diff --git a/test.ht b/test.ht deleted file mode 100644 index 58125a8..0000000 --- a/test.ht +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | # ht: a bespoke document preparation system | ||
2 | ;;@title="ht: a bespoke document preparation system" | ||
3 | ;; comments are like this. | ||
4 | ;; they're a good time. | ||
5 | |||
6 | `ht | ||
7 | is a quasi-line-based markup language that takes inspiration from | ||
8 | @https://gemini.circumlunar.space/docs/gemtext.gmi gemtext\ | ||
9 | , | ||
10 | @https://daringfireball.net/projects/markdown/ markdown\ | ||
11 | , and others. | ||
12 | Its aim is to be somewhat easy to read while being fairly easy to parse. | ||
13 | |||
14 | In fact, | ||
15 | `ht | ||
16 | is a simple awk script. | ||
17 | |||
18 | ## Usage | ||
19 | |||
20 | - one | ||
21 | - two | ||
22 | - three | ||
23 | |||
24 | ordered list: | ||
25 | |||
26 | % one | ||
27 | % two | ||
28 | % three | ||
29 | |||
30 | ``` | ||
31 | ./ht.awk source.ht | ||
32 | ``` | ||