Add CV
[~bandali/bndl.org] / ssng
... / ...
CommitLineData
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:=Amin Bandali}"
22: "${SERVER_NAME:=aminb.org}"
23: "${SERVER_PROTO:=https}"
24: "${RSS_AUTHOR:=amin@aminb.org}"
25: "${RSS_DESCRIPTION:=Personal website}"
26: "${COPYRIGHT_FROM_YEAR:=2016}"
27: "${DOCS:=out}"
28
29##########################################################################
30
31[ -n "$DOCS" ] || { echo "export DOCS <target_directory>"; exit 1; }
32DOCUMENT_ROOT=$(readlink -fn "$DOCS")
33TEMP_DIR=$(mktemp -d)
34# shellcheck disable=SC2064
35trap 'clean_up' EXIT
36trap exit HUP INT TERM
37[ "$2" = '--clean' ] && RSYNC_FLAGS='--delete-excluded' || RSYNC_FLAGS=''
38
39INDEX_HTML_FILE="$TEMP_DIR/index.html"
40CSS_FILE="$TEMP_DIR/styles.css"
41RSS_FILE="$TEMP_DIR/rss.xml"
42RSS_URL="$SERVER_PROTO://$SERVER_NAME/rss.xml"
43SITEMAP="$TEMP_DIR/sitemap.xml"
44
45ANNOUNCEMENT_FILE="$PWD/_announcement.html"
46FOOTER_FILE="$PWD/_footer.html"
47HEADER_FILE="$PWD/_header.html"
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>
55EOF
56)
57[ -f "$FOOTER_FILE" ] &&
58 FOOTER=$(cat "$FOOTER_FILE") ||
59 FOOTER=$(cat << EOF
60Copyright $COPYRIGHT_FROM_YEAR&ndash;$(date +%Y)
61<a href="/">$WEBSITE_TITLE</a>
62<span class="bar">|</span>
63<a href="/colophon">colophon</a>
64EOF
65)
66
67##########################################################################
68
69usage() {
70 echo 'usage: DOCS=<target_directory>'
71 echo
72 echo ' ssg build [--clean]'
73 echo ' | watch [--clean]'
74 exit 1
75}
76
77copy_to_temp_dir() {
78 rsync -a --delete-excluded \
79 --exclude '.*' \
80 --exclude '_*' \
81 '.' "$TEMP_DIR"
82}
83
84copy_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
95md_to_html() {
96 find "$TEMP_DIR" -type f -name '*.md'|
97 while read -r file; do
98 lowdown -D html-skiphtml -D html-head-ids \
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
107fst_h1='/<[h1]*( id=".*")?>/{gsub(/<[^>]*>/,"");print($0);exit;}'
108a='^<li><a href="\(.*\)" title="\([^<]*\)">[^<]*<\/a>.*<\/li>.*'
109
110line_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>
127EOF
128)|sed 's/\&nbsp;/\&#160;/'>>"$RSS_FILE"
129}
130
131index_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>
143EOF
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
152wrap_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
169xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
170xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
171http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
172xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
173EOF
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>
187EOF
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>
197EOF
198)"
199echo "$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"
208 ;;
209 /contact.html)
210 head_title="Contact | $WEBSITE_TITLE"
211 ;;
212 *)
213 title="${line#*====}"
214 head_title="$title | $WEBSITE_TITLE"
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
245<header>
246$HEADER
247</header>
248<div class="article clear">$article</div>
249<footer>$FOOTER</footer>
250</div>
251</body>
252</html>
253EOF
254 done
255 echo "$date $(echo "$sitemap"|wc -l|tr -d ' ')pp"
256}
257
258clean_up() { rm -rf "$TEMP_DIR"; }
259
260##########################################################################
261
262case "$1" in
263
264build)
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
280watch)
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
304esac
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.