| 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. |