sbs/sbs

298 lines
8.7 KiB
Bash
Executable File

#!/bin/sh
# sbs
# A simple and straightforward static site generator
# Copyright (C) 2022 Jake Bauer
# Licensed under the terms of the ISC License, see LICENSE for details.
set -o errexit # Halt processing if an error is encountered
set -o nounset # Do not allow the use of variables that haven't been set
if [ ! -x "$(command -v lowdown)" ]; then
printf "Error: The program 'lowdown' is needed but was not found.\n"
exit 1
fi
# Create a new page, post, or site with the following skeleton content
new()
{
if [ "$#" -lt 2 ]; then
printf "Please provide a subcommand. See sbs(1) for documentation.\n"
exit 1
fi
if [ "$#" -lt 3 ]; then
printf "Please provide a path. See sbs(1) for documentation.\n"
exit 1
fi
if [ "$2" = "page" ]; then
{ printf "Title: \nSummary: \n\n"
printf "# [%%title]\n\n"
} > "$3"
printf "Created: %s\n" "$3"
elif [ "$2" = "post" ]; then
{ printf "Title: \nAuthor: \nDate: \nSummary: \n\n"
printf "# [%%title]\n\n"
printf "**Author:** [%%author] | **Published:** [%%date]\n\n"
} > "$3"
printf "Created: %s\n" "$3"
elif [ "$2" = "site" ]; then
mkdir "$3" "$3/content/" "$3/static/" "$3/templates/"
touch "$3/static/style.css"
# Create template config.ini file
{ printf "siteURL = https://example.com/\n"
printf "siteName = %s\n" "$3"
printf "blogDir = blog/\n"
printf "languageCode = en\n"
printf "buildOptions = -Thtml --html-no-skiphtml --html-no-escapehtml\n"
printf "pushcmd = echo 'No command configured.'\n"
} > "$3/config.ini"
# Create template header.html file
{ printf '<!DOCTYPE html>\n'
printf '<html lang="">\n'
printf '<head>\n'
printf '\t<meta charset="utf-8">\n'
printf '\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
printf '\t<meta name="description" content="">\n'
printf '\t<link rel="stylesheet" href="/style.css">\n'
printf '\t<link rel="alternate" type="application/rss+xml" title="RSS feed" href="/feed.xml">\n'
printf '\t<title></title>\n'
printf '</head>\n'
printf '<body>\n'
printf '\t<header></header>\n'
printf '\t<main>\n'
} > "$3/templates/header.html"
# Create template footer.html file
{ printf '\t</main>\n'
printf '\t<footer></footer>\n'
printf '</body>\n'
printf '</html>\n'
} > "$3/templates/footer.html"
> "$3/static/style.css"
printf "Created: %s\n" "$3"
else
printf "Subcommand '%s' not recognized. See sbs(1) for documentation.\n" "$2"
exit 1
fi
exit 0
}
parse_configuration()
{
options="siteURL siteName blogDir languageCode buildOptions pushcmd"
for key in $options; do
value=$(grep "$key" config.ini | cut -d'=' -f2 | xargs)
if [ -n "$value" ]; then
eval "$key='$value'"
else
printf "Error: %s is not configured.\n" "$key"
exit 1
fi
done
# Validate configuration
if ! echo "$siteURL" | grep -qE '^https?://.*\..*/$'; then
echo "Error: siteURL should be in canonical form (e.g. https://example.com/).\n"
exit 1
fi
}
# Construct a complete atom feed from all blog posts
genfeed() {
{ printf '<?xml version="1.0" encoding="utf-8"?>\n'
printf '<feed xmlns="http://www.w3.org/2005/Atom">\n'
printf "\t<title>%s</title>\n" "$siteName"
printf "\t<link href=\"%s\" />\n" "$siteURL"
printf "\t<link rel=\"self\" href=\"%sfeed.xml\" />\n" "${siteURL}"
printf "\t<icon>/favicon.png</icon>\n"
printf "\t<updated>%s</updated>\n" "$(date +"%Y-%m-%dT%H:%M:%S%:z")"
printf "\t<id>%s</id>\n" "$siteURL"
printf "\t<generator>sbs</generator>\n\n"
} > static/feed.xml
tmp=$(mktemp)
find content/"$blogDir" -type f -name '*.md' | while read -r file; do
if [ -n "$(lowdown -X draft "$file" 2>/dev/null)" ]; then
continue
fi
printf "%s %s\n" "$(date -d "$(lowdown -X date "$file")" +"%s")" \
"$file" >> "$tmp"
done
sort -rn "$tmp" | cut -d' ' -f2 | while read -r file; do
fileName=$(basename "$file" .md).html
subDir=$(dirname "$file" | sed "s/content\///")
title=$(lowdown -X title "$file")
author=$(lowdown -X author "$file")
date=$(lowdown -X date "$file")
{ printf "\t<entry>\n"
printf "\t\t<title>%s</title>\n" "$title"
printf "\t\t<author><name>%s</name></author>\n" "$author"
printf "\t\t<link href=\"%s%s/%s\" />\n" "$siteURL" "$subDir" "$fileName"
printf "\t\t<id>%s%s/%s</id>\n" "$siteURL" "$subDir" "$fileName"
printf "\t\t<updated>%s</updated>\n" "$(date -d "$date" +"%Y-%m-%dT%H:%M:%S%:z")"
printf "\t\t<content type=\"html\"><![CDATA[\n%s\n\t\t]]></content>\n" "$(lowdown $buildOptions "$file")"
printf "\t</entry>\n\n"
} >> static/feed.xml
done
numEntries="$(wc -l "$tmp" | cut -d' ' -f1)"
printf '</feed>\n' >> static/feed.xml
printf "Created: static/feed.xml with %s entries.\n" "$numEntries"
rm "$tmp"
exit 0
}
# Build the pages given as arguments
build()
{
for file in "$@"; do
unset title
unset description
# Stop the filename from being prepended with the path multiple
# times as build() recurses
if ! echo "$file" | grep -q "$cwd"; then
file="$cwd"/"$file"
fi
if [ ! -f "$file" ]; then
if [ -d "$file" ]; then
build "$file"/*
continue
fi
printf "Error: %s does not exist. " "$file"
printf "Are you sure you're in the right directory?\n"
exit 1
fi
fileName=$(basename "$file" .md)
subDir=$(dirname "$file" | sed "s/.*\/content//")
mkdir -p "static/$subDir"
# Convert Gemtext files to Markdown
# The first sed expression converts all local links that end
# in .gmi to .html (e.g. /blog/post1.gmi --> /blog/post1.html).
# The second sed expression converts all gemini-style links to
# markdown-style links.
# The third sed expression fixes gemini links that only have a
# URL so that the URL will be displayed as the link text.
# The fourth sed expression converts links to images into
# Markdown's image syntax, so images will be displayed with the
# <img> tag.
if [ "$(echo "$file" | awk -F\. '{print $NF}' )" = "gmi" ]; then
printf "Converting: content%s/%s to markdown...\n" "$subDir" "$fileName"
fileName=$(basename "$file" .gmi)
sed -e 's/\(=>[ ]*\)\(.*\)\(.gmi\)\(.*\)/\1\2.html\4/g' \
-e 's/=>[ ]*\([^ ]*\)\( \|\)\(.*\)/[\3](\1)\n/g' \
-e 's/\[\](\(.*\))/[\1](\1)/g' \
-e 's/\(\[.*\]\)\((\(.*.jpe\?g\|.*.png\))\)/!\1\2/g' \
"$file" > /tmp/sbs/"$fileName".md
title=$(grep '^# ' "$file" | head -n1 | cut -d' ' -f2-)
description="Page auto-converted from the Gemini format."
file=/tmp/sbs/"$fileName".md
fi
printf "Creating: static%s/%s.html...\n" "$subDir" "$fileName"
# Extract metadata from markdown doc (if not converted from gmi)
title=${title:-$(lowdown -X title "$file")}
description=${description:-$(lowdown -X summary "$file")}
# Build and process the output document
lowdown $buildOptions "$file" \
| cat "templates/header.html" - "templates/footer.html" \
| sed -e "s/<title><\/title>/<title>$title - $siteName<\/title>/" \
-e "s/lang=\"\"/lang=\"$languageCode\"/" \
-e "s/content=\"\"/content=\"$description\"/" \
> "static/$subDir/$fileName".html
printf "Created: static%s/%s.html\n" "$subDir" "$fileName"
done
}
# Push the contents of the static/ folder using the configured command
push()
{
echo "$pushcmd"
sh -c "$pushcmd"
}
# Walks up the filesystem to the root of the website so it can be built from
# within any subdirectory. Has the side effect of making path-parsing more
# resilient.
walk_back()
{
# config.ini should be in the root of the website's folder structure
while [ ! -f config.ini ]; do
cd ..
if [ $(pwd) = "/" ]; then
printf "Error: Not inside of an sbs site directory.\n"
exit 1
fi
done
# Parse the config just for the siteName variable to ensure we're in the
# right place (in the root of the website's folder structure)
value=$(grep "siteName" config.ini | cut -d'=' -f2 | xargs)
if [ -n "$value" ]; then
eval "siteName='$value'"
else
printf "Error: siteName is not configured.\n"
exit 1
fi
# Check that we are in the root of the website's folder structure
if [ "$(basename $(pwd))" = "$siteName" ]; then
return 0
else
printf "Error: config.ini found but %s is not the root of the site.\n" "$(pwd)"
exit 1
fi
}
if [ "$#" -lt 1 ]; then
echo "Please provide a command. See sbs(1) for documentation."
exit 1
fi
case "$1" in
"build")
shift
# Store the current directory so we know where we started
cwd="$(pwd)"
walk_back
parse_configuration
mkdir -p /tmp/sbs/
# Allows simply running "sbs build" without path(s)
if [ $# -eq 0 ]; then
cwd=""
build ./content/*
else
build "$@"
fi
rm -rf /tmp/sbs/
;;
"genfeed")
walk_back
parse_configuration
genfeed
;;
"new")
new "$@"
;;
"push")
walk_back
parse_configuration
push
;;
"version")
echo "v0.6.0" ;
;;
*)
echo "Usage: sbs <command> [FILE ...]"
;;
esac
exit 0