Initial commit
[~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:=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; }
31DOCUMENT_ROOT=$(readlink -fn "$DOCS")
32TEMP_DIR=$(mktemp -d)
33# shellcheck disable=SC2064
34trap 'clean_up' EXIT
35trap exit HUP INT TERM
36[ "$2" = '--clean' ] && RSYNC_FLAGS='--delete-excluded' || RSYNC_FLAGS=''
37
38INDEX_HTML_FILE="$TEMP_DIR/index.html"
39CSS_FILE="$TEMP_DIR/styles.css"
40RSS_FILE="$TEMP_DIR/rss.xml"
41RSS_URL="$SERVER_PROTO://$SERVER_NAME/rss.xml"
42SITEMAP="$TEMP_DIR/sitemap.xml"
43
44ANNOUNCEMENT_FILE="$PWD/announcement.html"
45FOOTER_FILE="$PWD/footer.html"
46HEADER_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>
54EOF
55)
56[ -f "$FOOTER_FILE" ] &&
57 FOOTER=$(cat "$FOOTER_FILE") ||
58 FOOTER=$(cat << EOF
59Copyright $COPYRIGHT_YEAR&ndash;$(date +%Y)
60<a href="/about.html">$WEBSITE_TITLE</a>
61EOF
62)
63
64##########################################################################
65
66usage() {
67 echo 'usage: DOCS=<target_directory>'
68 echo
69 echo ' ssg build [--clean]'
70 echo ' | watch [--clean]'
71 exit 1
72}
73
74copy_to_temp_dir() {
75 rsync -a --delete-excluded \
76 --exclude '.*' \
77 --exclude '_*' \
78 '.' "$TEMP_DIR"
79}
80
81copy_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
92md_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
104fst_h1='/<[h1]*( id=".*")?>/{gsub(/<[^>]*>/,"");print($0);exit;}'
105a='^<li><a href="\(.*\)" title="\([^<]*\)">[^<]*<\/a>.*<\/li>.*'
106
107line_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>
124EOF
125)|sed 's/\&nbsp;/\&#160;/'>>"$RSS_FILE"
126}
127
128index_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>
140EOF
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
149wrap_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
166xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
167xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
168http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
169xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
170EOF
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>
184EOF
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>
194EOF
195)"
196echo "$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>
252EOF
253 done
254 echo "$date $(echo "$sitemap"|wc -l|tr -d ' ')pp"
255}
256
257clean_up() { rm -rf "$TEMP_DIR"; }
258
259##########################################################################
260
261case "$1" in
262
263build)
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
279watch)
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
303esac
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.