bin/ssg (view raw)
1#!/bin/sh -e
2#
3# https://rgz.ee/bin/ssg6
4# Copyright 2018-2019 Roman Zolotarev <hi@romanzolotarev.com>
5# Copyright 2022 la-ninpre <aaoth@aaoth.xyz>
6#
7# Permission to use, copy, modify, and/or distribute this software for any
8# purpose with or without fee is hereby granted, provided that the above
9# copyright notice and this permission notice appear in all copies.
10#
11# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18#
19
20main() {
21 test -n "$1" || usage
22 test -n "$2" || usage
23 test -n "$3" || usage
24 test -n "$4" || usage
25 test -n "$5" || usage
26 test -d "$1" || no_dir "$1"
27 test -d "$2" || no_dir "$2"
28 test -d "$3" || no_dir "$3"
29
30 src=$(readlink_f "$1")
31 dst=$(readlink_f "$2")
32 gemdst=$(readlink_f "$3")
33
34 IGNORE=$(
35 if ! test -f "$src/.ssgignore"; then
36 printf ' ! -path "*/.*"'
37 return
38 fi
39 while read -r x; do
40 test -n "$x" || continue
41 printf ' ! -path "*/%s*"' "$x"
42 done <"$src/.ssgignore"
43 )
44
45 # files
46
47 title="$4"
48
49 h_file="$src/_header.html"
50 f_file="$src/_footer.html"
51 gf_file="$src/_gemfooter.gmi"
52 test -f "$f_file" && FOOTER=$(cat "$f_file") && export FOOTER
53 test -f "$h_file" && HEADER=$(cat "$h_file") && export HEADER
54 test -f "$gf_file" && GEMFOOTER=$(cat "$gf_file") && export GEMFOOTER
55
56 list_dirs "$src" |
57 (cd "$src" && cpio -pdu "$dst")
58 list_dirs "$src" |
59 (cd "$src" && cpio -pdu "$gemdst")
60
61 fs=$(
62 if test -f "$dst/.files"; then
63 list_affected_files "$src" "$dst/.files"
64 if test -f "$gemdst/.files"; then
65 list_affected_files "$src" "$gemdst/.files"
66 fi
67 else
68 list_files "$1"
69 fi
70 )
71
72 if test -n "$fs"; then
73 echo "$fs" | tee "$dst/.files"
74 echo "$fs" | tee "$gemdst/.files"
75
76 if echo "$fs" | grep -q '\.md$'; then
77 if test -x "$(which lowdown 2>/dev/null)"; then
78 echo "$fs" | grep '\.md$' |
79 render_md_files_lowdown "$src" "$dst" "$title"
80 echo "$fs" | grep '\.md$' |
81 render_md_files_lowdown_gemini "$src" "$gemdst"
82
83 else
84 echo "couldn't find lowdown nor Markdown.pl"
85 exit 3
86 fi
87 fi
88
89 echo "$fs" | grep '\.html$' |
90 render_html_files "$src" "$dst" "$title"
91
92 echo "$fs" | grep -Ev '\.md$|\.html$' |
93 (cd "$src" && cpio -pu "$dst")
94 echo "$fs" | grep -Ev '\.md$|\.gmi$' |
95 (cd "$src" && cpio -pu "$gemdst")
96 fi
97
98 printf '[ssg] ' >&2
99 print_status 'file, ' 'files, ' "$fs" >&2
100
101 # sitemap
102
103 base_url="$5"
104 date=$(date +%Y-%m-%d)
105 urls=$(list_pages "$src")
106
107 test -n "$urls" &&
108 render_sitemap "$urls" "$base_url" "$date" >"$dst/sitemap.xml"
109
110 print_status 'url' 'urls' "$urls" >&2
111 echo >&2
112}
113
114readlink_f() {
115 file="$1"
116 cd "$(dirname "$file")"
117 file=$(basename "$file")
118 while test -L "$file"; do
119 file=$(readlink "$file")
120 cd "$(dirname "$file")"
121 file=$(basename "$file")
122 done
123 dir=$(pwd -P)
124 echo "$dir/$file"
125}
126
127print_status() {
128 test -z "$3" && printf 'no %s' "$2" && return
129
130 echo "$3" | awk -v singular="$1" -v plural="$2" '
131 END {
132 if (NR==1) printf NR " " singular
133 if (NR>1) printf NR " " plural
134 }'
135}
136
137usage() {
138 echo "usage: ${0##*/} src dst gemdst title base_url" >&2
139 exit 1
140}
141
142no_dir() {
143 echo "${0##*/}: $1: No such directory" >&2
144 exit 2
145}
146
147list_dirs() {
148 cd "$1" && eval "find . -type d ! -name '.' ! -path '*/_*' $IGNORE"
149}
150
151list_files() {
152 cd "$1" && eval "find . -type f ! -name '.' ! -path '*/_*' $IGNORE"
153}
154
155list_dependant_files() {
156 e="\\( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \\)"
157 cd "$1" && eval "find . -type f ! -name '.' ! -path '*/_*' $IGNORE $e"
158}
159
160list_newer_files() {
161 cd "$1" && eval "find . -type f ! -name '.' $IGNORE -newer $2"
162}
163
164has_partials() {
165 grep -qE '^./_.*\.html$|^./_.*\.js$|^./_.*\.css$'
166}
167
168list_affected_files() {
169 fs=$(list_newer_files "$1" "$2")
170
171 if echo "$fs" | has_partials; then
172 list_dependant_files "$1"
173 else
174 echo "$fs"
175 fi
176}
177
178render_html_files() {
179 while read -r f; do
180 render_html_file "$3" <"$1/$f" >"$2/$f"
181 done
182}
183
184render_md_files_lowdown() {
185 while read -r f; do
186 lowdown \
187 --html-no-escapehtml \
188 --html-no-skiphtml \
189 --parse-no-metadata \
190 --parse-no-autolink <"$1/$f" |
191 render_html_file "$3" \
192 >"$2/${f%\.md}.html"
193 done
194}
195
196render_md_files_lowdown_gemini() {
197 while read -r f; do
198 lowdown \
199 -Tgemini <"$1/$f" |
200 render_gmi_file \
201 >"$2/${f%\.md}.gmi"
202 done
203}
204
205render_html_file() {
206 # h/t Devin Teske
207 awk -v title="$1" '
208 { body = body "\n" $0 }
209 END {
210 body = substr(body, 2)
211 if (body ~ /<\/?[Hh][Tt][Mm][Ll]/) {
212 print body
213 exit
214 }
215 if (match(body, /<[[:space:]]*[Hh]1(>|[[:space:]][^>]*>)/)) {
216 t = substr(body, RSTART + RLENGTH)
217 sub("<[[:space:]]*/[[:space:]]*[Hh]1.*", "", t)
218 gsub(/^[[:space:]]*|[[:space:]]$/, "", t)
219 if (t) title = t " — " title
220 }
221 n = split(ENVIRON["HEADER"], header, /\n/)
222 for (i = 1; i <= n; i++) {
223 if (match(tolower(header[i]), "<title></title>")) {
224 head = substr(header[i], 1, RSTART - 1)
225 tail = substr(header[i], RSTART + RLENGTH)
226 print head "<title>" title "</title>" tail
227 } else print header[i]
228 }
229 print body
230 print ENVIRON["FOOTER"]
231 }'
232}
233
234render_gmi_file() {
235 awk '
236 { body = body "\n" $0 }
237 END {
238 body = substr(body, 2)
239 n = split(body, body_n, /\n/)
240 for (i = 1; i <= n; i++) {
241 if (!match(body_n[i], /^=>[[:space:]]*[Hh][Tt]{2}[Pp][Ss]?:\/\/.*/)) {
242 sub(/\.html[[:space:]]*/, ".gmi ", body_n[i])
243 }
244 print body_n[i]
245 }
246 #print body
247 print ENVIRON["GEMFOOTER"]
248 }'
249}
250
251list_pages() {
252 e="\\( -name '*.html' -o -name '*.md' \\)"
253 cd "$1" && eval "find . -type f ! -path '*/.*' ! -path '*/_*' $IGNORE $e" |
254 sed 's#^./##;s#.md$#.html#;s#/index.html$#/#'
255}
256
257render_sitemap() {
258 urls="$1"
259 base_url="$2"
260 date="$3"
261
262 echo '<?xml version="1.0" encoding="UTF-8"?>'
263 echo '<urlset'
264 echo 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
265 echo 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9'
266 echo 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"'
267 echo 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
268 echo "$urls" |
269 sed -E 's#^(.*)$#<url><loc>'"$base_url"'/\1</loc><lastmod>'"$date"'</lastmod><priority>1.0</priority></url>#'
270 echo '</urlset>'
271}
272
273main "$@"