Commit | Line | Data |
---|---|---|
b7b51be5 AB |
1 | #!/bin/sh |
2 | ||
3 | # Copyright (C) 2018 Amin Bandali <amin@aminb.org> | |
4 | ||
5 | # This program is free software: you can redistribute it and/or modify | |
6 | # it under the terms of the GNU General Public License as published by | |
7 | # the Free Software Foundation, either version 3 of the License, or | |
8 | # (at your option) any later version. | |
9 | ||
10 | # This program is distributed in the hope that it will be useful, | |
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | ||
15 | # You should have received a copy of the GNU General Public License | |
16 | # along with this program. If not, see <https://www.gnu.org/licenses/>. | |
17 | ||
18 | # ssng is a fork of Roman Zolotarev's ssg. See end of file for ssg's | |
19 | # license notice. | |
20 | ||
2c35d3a8 AB |
21 | : "${WEBSITE_TITLE:=Amin Bandali}" |
22 | : "${SERVER_NAME:=aminb.org}" | |
b7b51be5 | 23 | : "${SERVER_PROTO:=https}" |
2c35d3a8 | 24 | : "${RSS_AUTHOR:=amin@aminb.org}" |
b7b51be5 | 25 | : "${RSS_DESCRIPTION:=Personal website}" |
2c35d3a8 | 26 | : "${COPYRIGHT_FROM_YEAR:=2016}" |
3b86855a | 27 | : "${DOCS:=out}" |
b7b51be5 AB |
28 | |
29 | ########################################################################## | |
30 | ||
31 | [ -n "$DOCS" ] || { echo "export DOCS <target_directory>"; exit 1; } | |
32 | DOCUMENT_ROOT=$(readlink -fn "$DOCS") | |
33 | TEMP_DIR=$(mktemp -d) | |
34 | # shellcheck disable=SC2064 | |
35 | trap 'clean_up' EXIT | |
36 | trap exit HUP INT TERM | |
37 | [ "$2" = '--clean' ] && RSYNC_FLAGS='--delete-excluded' || RSYNC_FLAGS='' | |
38 | ||
39 | INDEX_HTML_FILE="$TEMP_DIR/index.html" | |
40 | CSS_FILE="$TEMP_DIR/styles.css" | |
41 | RSS_FILE="$TEMP_DIR/rss.xml" | |
42 | RSS_URL="$SERVER_PROTO://$SERVER_NAME/rss.xml" | |
43 | SITEMAP="$TEMP_DIR/sitemap.xml" | |
44 | ||
3b86855a AB |
45 | ANNOUNCEMENT_FILE="$PWD/_announcement.html" |
46 | FOOTER_FILE="$PWD/_footer.html" | |
47 | HEADER_FILE="$PWD/_header.html" | |
b7b51be5 AB |
48 | [ -f "$ANNOUNCEMENT_FILE" ] && |
49 | ANNOUNCEMENT_TEXT=$(cat "$ANNOUNCEMENT_FILE") | |
50 | [ -f "$HEADER_FILE" ] && | |
51 | HEADER=$(cat "$HEADER_FILE") || | |
52 | HEADER=$(cat << EOF | |
53 | <a href="/">Home</a> - | |
54 | <a href="/twitter.html">Twitter</a> | |
55 | EOF | |
56 | ) | |
57 | [ -f "$FOOTER_FILE" ] && | |
58 | FOOTER=$(cat "$FOOTER_FILE") || | |
59 | FOOTER=$(cat << EOF | |
3b86855a AB |
60 | Copyright $COPYRIGHT_FROM_YEAR–$(date +%Y) |
61 | <a href="/">$WEBSITE_TITLE</a> | |
62 | <span class="bar">|</span> | |
63 | <a href="/colophon">colophon</a> | |
b7b51be5 AB |
64 | EOF |
65 | ) | |
66 | ||
67 | ########################################################################## | |
68 | ||
69 | usage() { | |
70 | echo 'usage: DOCS=<target_directory>' | |
71 | echo | |
72 | echo ' ssg build [--clean]' | |
73 | echo ' | watch [--clean]' | |
74 | exit 1 | |
75 | } | |
76 | ||
77 | copy_to_temp_dir() { | |
78 | rsync -a --delete-excluded \ | |
79 | --exclude '.*' \ | |
80 | --exclude '_*' \ | |
81 | '.' "$TEMP_DIR" | |
82 | } | |
83 | ||
84 | copy_to_document_root() { | |
85 | [ "$(dirname "$DOCUMENT_ROOT")" = "$PWD" ] && | |
86 | self="/$(basename "$DOCUMENT_ROOT")/" || | |
87 | self="$DOCUMENT_ROOT" | |
88 | rsync -a $RSYNC_FLAGS \ | |
89 | --exclude "$self" \ | |
90 | --exclude '.*' \ | |
91 | --exclude '_*' \ | |
92 | "$TEMP_DIR/" "$DOCUMENT_ROOT" | |
93 | } | |
94 | ||
95 | md_to_html() { | |
96 | find "$TEMP_DIR" -type f -name '*.md'| | |
97 | while read -r file; do | |
3b86855a | 98 | lowdown -D html-skiphtml -D html-head-ids \ |
b7b51be5 AB |
99 | "$file" > "${file%\.md}.html" && |
100 | rm "$file" | |
101 | done | |
102 | } | |
103 | ||
104 | ||
105 | # filter first 20 lines with links and link titles (dates) | |
106 | # shellcheck disable=SC2016 | |
107 | fst_h1='/<[h1]*( id=".*")?>/{gsub(/<[^>]*>/,"");print($0);exit;}' | |
108 | a='^<li><a href="\(.*\)" title="\([^<]*\)">[^<]*<\/a>.*<\/li>.*' | |
109 | ||
110 | line_to_rss_item() { | |
111 | url=$(echo "$line"|sed "s/$a/\\1/g") | |
112 | date=$(echo "$line"|sed "s/$a/\\2/g") | |
113 | file="${TEMP_DIR}${url}" | |
114 | [ ! -f "$file" ] && return | |
115 | ||
116 | title="$(awk "$fst_h1" "$file")" | |
117 | # replace relative URIs with absolute URIs | |
118 | article=$(sed "s/\\([hrefsc]*\\)=\"\\//\\1=\"$prefix/g" "$file") | |
119 | echo $(cat << EOF | |
120 | <item> | |
121 | <title>$title</title> | |
122 | <guid>$SERVER_PROTO://${SERVER_NAME}$url</guid> | |
123 | <link>$SERVER_PROTO://${SERVER_NAME}$url</link> | |
124 | <pubDate>$date 00:00:00 +0000</pubDate> | |
125 | <description><![CDATA[$article]]></description> | |
126 | </item> | |
127 | EOF | |
128 | )|sed 's/\ /\ /'>>"$RSS_FILE" | |
129 | } | |
130 | ||
131 | index_to_rss() { | |
132 | date_rfc_822=$(date "+%a, %d %b %Y %H:%M:%S %z") | |
133 | cat > "$RSS_FILE" << EOF | |
134 | <?xml version="1.0" encoding="utf-8"?> | |
135 | <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> | |
136 | <channel> | |
137 | <atom:link href="$RSS_URL" rel="self" type="application/rss+xml" /> | |
138 | <title>$WEBSITE_TITLE</title> | |
139 | <description>$RSS_DESCRIPTION</description> | |
140 | <link>$SERVER_PROTO://$SERVER_NAME/</link> | |
141 | <lastBuildDate>$date_rfc_822</lastBuildDate> | |
142 | <managingEditor>$RSS_AUTHOR</managingEditor> | |
143 | EOF | |
144 | ||
145 | prefix="$SERVER_PROTO:\\/\\/$SERVER_NAME\\/" | |
146 | grep "$a" "$INDEX_HTML_FILE" | | |
147 | head -n20 | | |
148 | while read -r line; do line_to_rss_item "$line"; done | |
149 | echo '</channel></rss>' >> "$RSS_FILE" | |
150 | } | |
151 | ||
152 | wrap_html() { | |
153 | # generate sorted sitemap | |
154 | find_h1_tag='/<[h1]*( id=".*")?>/' | |
155 | # shellcheck disable=SC2016 | |
156 | tag_content='{gsub(/<[^>]*>/,"");print(FILENAME"===="$0);exit;}' | |
157 | sitemap="$( | |
158 | find "$TEMP_DIR" -type f -name '*.html'| | |
159 | while read -r file; do | |
160 | awk "${find_h1_tag}${tag_content}" "$file" | |
161 | done| | |
162 | sort | |
163 | )" | |
164 | # save sitemap in html and xml formats | |
165 | date=$(date +%Y-%m-%dT%H:%M:%S%z) | |
166 | cat > "$SITEMAP" << EOF | |
167 | <?xml version="1.0" encoding="UTF-8"?> | |
168 | <urlset | |
169 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
170 | xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 | |
171 | http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" | |
172 | xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | |
173 | EOF | |
174 | echo "$sitemap"|while read -r line; do | |
175 | page=${line%====*} | |
176 | url=${page#$TEMP_DIR} | |
177 | case "$url" in | |
178 | /index.html) title='Home';; | |
179 | *) title="${line#*====}";; | |
180 | esac | |
181 | cat >> "$SITEMAP" << EOF | |
182 | <url> | |
183 | <loc>$SERVER_PROTO://${SERVER_NAME}$url</loc> | |
184 | <lastmod>$date</lastmod> | |
185 | <priority>1.0</priority> | |
186 | </url> | |
187 | EOF | |
188 | done | |
189 | echo '</urlset>' >> "$SITEMAP" | |
190 | # generate html pages | |
191 | styles=$(cat "$CSS_FILE") | |
192 | [ -n "$ANNOUNCEMENT_TEXT" ] && | |
193 | announcement="$(cat << EOF | |
194 | <div class="announcement"> | |
195 | <div class="announcement__text">$ANNOUNCEMENT_TEXT</div> | |
196 | </div> | |
197 | EOF | |
198 | )" | |
199 | echo "$sitemap"| | |
200 | while read -r line; do | |
201 | page=${line%====*} | |
202 | url=${page#$TEMP_DIR} | |
203 | article=$(cat "$page") | |
204 | case "$url" in | |
205 | /index.html) | |
206 | title='Home' | |
207 | head_title="$WEBSITE_TITLE" | |
3b86855a AB |
208 | ;; |
209 | /contact.html) | |
210 | head_title="Contact | $WEBSITE_TITLE" | |
b7b51be5 AB |
211 | ;; |
212 | *) | |
213 | title="${line#*====}" | |
3b86855a | 214 | head_title="$title | $WEBSITE_TITLE" |
b7b51be5 AB |
215 | ;; |
216 | esac | |
217 | # merge page with html template | |
218 | cat > "$page" <<EOF | |
219 | <!DOCTYPE html><html lang="en"> | |
220 | <head><title>$head_title</title> | |
221 | <meta charset="utf-8"> | |
222 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
223 | <link rel="alternate" type="application/atom+xml" href="/rss.xml"> | |
224 | <link rel="icon" type="image/png" href="/favicon.png"> | |
225 | <style>$styles</style> | |
226 | </head> | |
227 | <body> | |
228 | <script> | |
229 | !function(t){ | |
230 | t.addEventListener('DOMContentLoaded', function () { | |
231 | var l = t.querySelector('#light-off'); | |
232 | if (l === null) { console.log('Lights-out...'); } | |
233 | else { | |
234 | l.checked = t.cookie.match(/lightOff=true/) !== null; | |
235 | l.addEventListener('change', function () { | |
236 | t.cookie = 'lightOff=' + JSON.stringify(l.checked) + ';path=/'; | |
237 | }); | |
238 | } | |
239 | }) | |
240 | }(document); | |
241 | </script> | |
242 | <input class="light-off" type="checkbox" id="light-off"> | |
243 | <div class="page"> | |
244 | $announcement | |
3b86855a AB |
245 | <header> |
246 | $HEADER | |
247 | </header> | |
248 | <div class="article clear">$article</div> | |
249 | <footer>$FOOTER</footer> | |
b7b51be5 AB |
250 | </div> |
251 | </body> | |
252 | </html> | |
253 | EOF | |
254 | done | |
255 | echo "$date $(echo "$sitemap"|wc -l|tr -d ' ')pp" | |
256 | } | |
257 | ||
258 | clean_up() { rm -rf "$TEMP_DIR"; } | |
259 | ||
260 | ########################################################################## | |
261 | ||
262 | case "$1" in | |
263 | ||
264 | build) | |
265 | ls index.* >/dev/null 2>&1 || | |
266 | { echo 'no index.* found in the directory'; exit 1; } | |
267 | [ ! -x "$(which rsync)" ] && | |
268 | { echo 'rsync(1) should be installed'; exit 1; } | |
269 | [ ! -x "$(which lowdown)" ] && | |
270 | { echo 'lowdown(1) should be installed'; exit 1; } | |
271 | printf 'building %s %s ' "$DOCUMENT_ROOT" "$2" | |
272 | copy_to_temp_dir | |
273 | md_to_html | |
274 | index_to_rss | |
275 | wrap_html | |
276 | copy_to_document_root | |
277 | clean_up | |
278 | ;; | |
279 | ||
280 | watch) | |
281 | cmd="entr -d env DOCS=$DOCS $(basename "$0") build $2" | |
282 | pgrep -qf "$cmd" && { echo "already watching $DOCS"; exit 1; } | |
283 | echo "watching $PWD" | |
284 | [ ! -x "$(which entr)" ] && | |
285 | { echo 'entr(1) should be installed'; exit 1; } | |
286 | while true; do | |
287 | find "$PWD" -type f \ | |
288 | \( -name "$(basename "$0")" \ | |
289 | -or -name '*.md' \ | |
290 | -or -name '*.html' \ | |
291 | -or -name '*.css' \ | |
292 | -or -name '*.txt' \ | |
293 | -or -name '*.jpeg' \ | |
294 | -or -name '*.png' \)\ | |
295 | ! -name ".*" \ | |
296 | ! -path "*/.*" \ | |
297 | ! -path "${DOCUMENT_ROOT}*" | | |
298 | $cmd | |
299 | done | |
300 | ;; | |
301 | ||
302 | *) usage;; | |
303 | ||
304 | esac | |
305 | ||
306 | ||
307 | ## ssg's license: ## | |
308 | ||
309 | # https://www.romanzolotarev.com/bin/ssg | |
310 | # Copyright 2018 Roman Zolotarev <hi@romanzolotarev.com> | |
311 | # | |
312 | # Permission to use, copy, modify, and/or distribute this software for any | |
313 | # purpose with or without fee is hereby granted, provided that the above | |
314 | # copyright notice and this permission notice appear in all copies. | |
315 | # | |
316 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
317 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
318 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
319 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
320 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
321 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |
322 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |