sbs

A Simple Blogging System.
git clone https://git.sr.ht/~jbauer/sbs
Log | Files | Refs | README | LICENSE

sbs (9495B)


      1 #!/bin/sh
      2 
      3 # sbs
      4 # A simple and straightforward static site generator
      5 # Copyright (C) 2022  Jake Bauer
      6 # Licensed under the terms of the ISC License, see LICENSE for details.
      7 
      8 set -o errexit # Halt processing if an error is encountered
      9 set -o nounset # Do not allow the use of variables that haven't been set
     10 
     11 if [ ! -x "$(command -v lowdown)" ]; then
     12 	printf "Error: The program 'lowdown' is needed but was not found.\n"
     13 	exit 1
     14 fi
     15 
     16 # Create a new page, post, or site with the following skeleton content
     17 new()
     18 {
     19 	if [ "$#" -lt 2 ]; then
     20 		printf "Please provide a subcommand. See sbs(1) for documentation.\n"
     21 		exit 1
     22 	fi
     23 
     24 	if [ "$#" -lt 3 ]; then
     25 		printf "Please provide a path. See sbs(1) for documentation.\n"
     26 		exit 1
     27 	fi
     28 
     29 	if [ -e "$3" ]; then
     30 		printf "%s already exists. Doing this will overwrite it.\n" "$3"
     31 		printf "Are you sure you want to overwrite it? (y/N): "
     32 		read input
     33 		input=$(echo "$input" | tr "[:upper:]" "[:lower:]")
     34 		if [ "$input" != "y" ] && [ "$input" != "yes" ]; then
     35 			return
     36 		fi
     37 	fi
     38 
     39 	if [ "$2" = "page" ]; then
     40 		{ printf "Title: \nSummary: \n\n"
     41 		  printf "# [%%title]\n\n"
     42 		} > "$3"
     43 		printf "Created: %s\n" "$3"
     44 	elif [ "$2" = "post" ]; then
     45 		{ printf "Title: \nAuthor: \nDate: \nSummary: \n\n"
     46 		  printf "# [%%title]\n\n"
     47 		  printf "**Author:** [%%author] | **Published:** [%%date]\n\n"
     48 		} > "$3"
     49 		printf "Created: %s\n" "$3"
     50 	elif [ "$2" = "site" ]; then
     51 		mkdir "$3" "$3/content/" "$3/static/" "$3/templates/"
     52 		touch "$3/static/style.css"
     53 		# Create template config.ini file
     54 		{ printf "siteURL = https://example.com/\n"
     55 		  printf "siteName = %s\n" "$3"
     56 		  printf "blogDir = blog/\n"
     57 		  printf "languageCode = en\n"
     58 		  printf "buildOptions = -Thtml --html-no-skiphtml --html-no-escapehtml\n"
     59 		  printf "pushcmd = echo 'No command configured.'\n"
     60 		} > "$3/config.ini"
     61 		# Create template header.html file
     62 		{ printf '<!DOCTYPE html>\n'
     63 		  printf '<html lang="">\n'
     64 		  printf '<head>\n'
     65 		  printf '\t<meta charset="utf-8">\n'
     66 		  printf '\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
     67 		  printf '\t<meta name="description" content="">\n'
     68 		  printf '\t<link rel="stylesheet" href="/style.css">\n'
     69 		  printf '\t<link rel="alternate" type="application/rss+xml" title="RSS feed" href="/feed.xml">\n'
     70 		  printf '\t<title></title>\n'
     71 		  printf '</head>\n'
     72 		  printf '<body>\n'
     73 		  printf '\t<header></header>\n'
     74 		  printf '\t<main>\n'
     75 		} > "$3/templates/header.html"
     76 		# Create template footer.html file
     77 		{ printf '\t</main>\n'
     78 		  printf '\t<footer></footer>\n'
     79 		  printf '</body>\n'
     80 		  printf '</html>\n'
     81 		} > "$3/templates/footer.html"
     82 		> "$3/static/style.css"
     83 		printf "Created: %s\n" "$3"
     84 	else
     85 		printf "Subcommand '%s' not recognized. See sbs(1) for documentation.\n" "$2"
     86 		exit 1
     87 	fi
     88 	exit 0
     89 }
     90 
     91 parse_configuration()
     92 {
     93 	options="siteURL siteName blogDir languageCode buildOptions pushcmd"
     94 	for key in $options; do
     95 		value=$(grep "$key" config.ini | cut -d'=' -f2 | xargs)
     96 		if [ -n "$value" ]; then
     97 			eval "$key='$value'"
     98 		else
     99 			printf "Error: %s is not configured.\n" "$key"
    100 			exit 1
    101 		fi
    102 	done
    103 
    104 	# Validate configuration
    105 	if ! echo "$siteURL" | grep -qE '^https?://.*\..*/$'; then
    106 		echo "Error: siteURL should be in canonical form (e.g. https://example.com/).\n"
    107 		exit 1
    108 	fi
    109 }
    110 
    111 # Construct a complete atom feed from all blog posts
    112 genfeed()
    113 {
    114 	{ printf '<?xml version="1.0" encoding="utf-8"?>\n'
    115 	  printf '<feed xmlns="http://www.w3.org/2005/Atom">\n'
    116 	  printf "\t<title>%s</title>\n" "$siteName"
    117 	  printf "\t<link href=\"%s\" />\n" "$siteURL"
    118 	  printf "\t<link rel=\"self\" href=\"%sfeed.xml\" />\n" "${siteURL}"
    119 	  printf "\t<icon>/favicon.png</icon>\n"
    120 	  printf "\t<updated>%s</updated>\n" "$(date +"%Y-%m-%dT%H:%M:%S%:z")"
    121 	  printf "\t<id>%s</id>\n" "$siteURL"
    122 	  printf "\t<generator>sbs</generator>\n\n"
    123 	} > static/feed.xml
    124 
    125 	tmp=$(mktemp)
    126 	find content/"$blogDir" -type f -name '*.md' -not -name index.md | while read -r file; do
    127 		if [ -n "$(lowdown -X draft "$file" 2>/dev/null)" ]; then
    128 			continue
    129 		fi
    130 		printf "%s %s\n" "$(date -d "$(lowdown -X date "$file")" +"%s")" \
    131 			"$file" >> "$tmp"
    132 	done
    133 	sort -rn "$tmp" | cut -d' ' -f2 | while read -r file; do
    134 		fileName=$(basename "$file" .md).html
    135 		subDir=$(dirname "$file" | sed "s/content\///")
    136 
    137 		title=$(lowdown -X title "$file")
    138 		author=$(lowdown -X author "$file")
    139 		date=$(lowdown -X date "$file")
    140 
    141 		{ printf "\t<entry>\n"
    142 		  printf "\t\t<title>%s</title>\n" "$title"
    143 		  printf "\t\t<author><name>%s</name></author>\n" "$author"
    144 		  printf "\t\t<link href=\"%s%s/%s\" />\n" "$siteURL" "$subDir" "$fileName"
    145 		  printf "\t\t<id>%s%s/%s</id>\n" "$siteURL" "$subDir" "$fileName"
    146 		  printf "\t\t<updated>%s</updated>\n" "$(date -d "$date" +"%Y-%m-%dT%H:%M:%S%:z")"
    147 		  printf "\t\t<content type=\"html\"><![CDATA[\n%s\n\t\t]]></content>\n" "$(lowdown $buildOptions "$file")"
    148 		  printf "\t</entry>\n\n"
    149 		} >> static/feed.xml
    150 	done
    151 
    152 	numEntries="$(wc -l "$tmp" | cut -d' ' -f1)"
    153 	printf '</feed>\n' >> static/feed.xml
    154 	printf "Created: static/feed.xml with %s entries.\n" "$numEntries"
    155 	rm "$tmp"
    156 	exit 0
    157 }
    158 
    159 # Build the pages given as arguments
    160 build()
    161 {
    162 	for file in "$@"; do
    163 		unset title
    164 		unset description
    165 		# Stop the filename from being prepended with the path multiple
    166 		# times as build() recurses
    167 		if ! echo "$file" | grep -q "$cwd"; then
    168 			file="$cwd"/"$file"
    169 		fi
    170 		if [ ! -f "$file" ]; then
    171 			if [ -d "$file" ]; then
    172 				build "$file"/*
    173 				continue
    174 			fi
    175 			printf "Error: %s does not exist. " "$file"
    176 			printf "Are you sure you're in the right directory?\n"
    177 			exit 1
    178 		fi
    179 
    180 		fileName=$(basename "$file" .md)
    181 		subDir=$(dirname "$file" | sed "s/.*\/content//")
    182 		mkdir -p "static/$subDir"
    183 
    184 		# Convert Gemtext files to Markdown
    185 		# The first sed expression converts all local links that end
    186 		# in .gmi to .html (e.g. /blog/post1.gmi --> /blog/post1.html).
    187 		# The second sed expression converts all gemini-style links to
    188 		# markdown-style links.
    189 		# The third sed expression fixes gemini links that only have a
    190 		# URL so that the URL will be displayed as the link text.
    191 		# The fourth sed expression converts links to images into
    192 		# Markdown's image syntax, so images will be displayed with the
    193 		# <img> tag.
    194 		if [ "$(echo "$file" | awk -F\. '{print $NF}' )" = "gmi" ]; then
    195 			printf "Converting: content%s/%s to markdown...\n" "$subDir" "$fileName"
    196 			fileName=$(basename "$file" .gmi)
    197 			sed -e 's/\(=>[ ]*\)\(.*\)\(.gmi\)\(.*\)/\1\2.html\4/g' \
    198 				-e 's/=>[ ]*\([^ ]*\)\( \|\)\(.*\)/[\3](\1)\n/g' \
    199 				-e 's/\[\](\(.*\))/[\1](\1)/g' \
    200 				-e 's/\(\[.*\]\)\((\(.*.jpe\?g\|.*.png\))\)/!\1\2/g' \
    201 				"$file" > /tmp/sbs/"$fileName".md
    202 			title=$(grep '^# ' "$file" | head -n1 | cut -d' ' -f2-)
    203 			description="Page auto-converted from the Gemini format."
    204 			file=/tmp/sbs/"$fileName".md
    205 		fi
    206 
    207 		printf "Creating: static%s/%s.html...\n" "$subDir" "$fileName"
    208 
    209 		# Extract metadata from markdown doc (if not converted from gmi)
    210 		title=${title:-$(lowdown -X title "$file")}
    211 		description=${description:-$(lowdown -X summary "$file")}
    212 
    213 		# Escapes characters from text that might interfere with sed
    214 		title=$(echo $title | sed 's/\\/\\\\/g; s/\//\\\//g; s/\^/\\^/g;
    215 		s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')
    216 		description=$(echo $description | sed 's/\\/\\\\/g; s/\//\\\//g;
    217 		s/\^/\\^/g; s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')
    218 
    219 		# Build and process the output document
    220 		lowdown $buildOptions "$file" \
    221 			| cat "templates/header.html" - "templates/footer.html" \
    222 			| sed -e "s/<title><\/title>/<title>$title - $siteName<\/title>/" \
    223 			-e "s/lang=\"\"/lang=\"$languageCode\"/" \
    224 			-e "s/content=\"\"/content=\"$description\"/" \
    225 			> "static/$subDir/$fileName".html
    226 
    227 		printf "Created: static%s/%s.html\n" "$subDir" "$fileName"
    228 	done
    229 }
    230 
    231 # Push the contents of the static/ folder using the configured command
    232 push()
    233 {
    234 	echo "$pushcmd"
    235 	sh -c "$pushcmd"
    236 }
    237 
    238 # Walks up the filesystem to the root of the website so it can be built from
    239 # within any subdirectory. Has the side effect of making path-parsing more
    240 # resilient.
    241 walk_back()
    242 {
    243 	# config.ini should be in the root of the website's folder structure
    244 	while [ ! -f config.ini ]; do
    245 		cd ..
    246 		if [ $(pwd) = "/" ]; then
    247 			printf "Error: Not inside of an sbs site directory.\n"
    248 			exit 1
    249 		fi
    250 	done
    251 
    252 	# Parse the config just for the siteName variable to ensure we're in the
    253 	# right place (in the root of the website's folder structure)
    254 	value=$(grep "siteName" config.ini | cut -d'=' -f2 | xargs)
    255 	if [ -n "$value" ]; then
    256 		eval "siteName='$value'"
    257 	else
    258 		printf "Error: siteName is not configured.\n"
    259 		exit 1
    260 	fi
    261 
    262 	# Check that we are in the root of the website's folder structure
    263 	if [ "$(basename $(pwd))" = "$siteName" ]; then
    264 		return 0
    265 	else
    266 		printf "Error: config.ini found but %s is not the root of the site.\n" "$(pwd)"
    267 		exit 1
    268 	fi
    269 }
    270 
    271 if [ "$#" -lt 1 ]; then
    272 	echo "Please provide a command. See sbs(1) for documentation."
    273 	exit 1
    274 fi
    275 
    276 case "$1" in
    277 	"build")
    278 		shift
    279 		# Store the current directory so we know where we started
    280 		cwd="$(pwd)"
    281 		walk_back
    282 		parse_configuration
    283 		mkdir -p /tmp/sbs/
    284 		# Allows simply running "sbs build" without path(s)
    285 		if [ $# -eq 0 ]; then
    286 			cwd=""
    287 			build ./content/*
    288 		else
    289 			build "$@"
    290 		fi
    291 		rm -rf /tmp/sbs/
    292 		;;
    293 	"genfeed")
    294 		walk_back
    295 		parse_configuration
    296 		genfeed
    297 		;;
    298 	"new")
    299 		new "$@"
    300 		;;
    301 	"push")
    302 		walk_back
    303 		parse_configuration
    304 		push
    305 		;;
    306 	"version")
    307 		echo "v0.7.0" ;
    308 		;;
    309 	*)
    310 		echo "Usage: sbs <command> [FILE ...]"
    311 		;;
    312 esac
    313 
    314 exit 0