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