sbs

A Simple Blogging System.
git clone https://git.jaderune.net/jbauer/sbs
Log | Files | Refs | README | LICENSE

sbs (9079B)


      1 #!/bin/sh
      2 
      3 # sbs
      4 # A simple and straightforward static site generator
      5 # Copyright (C) 2021-2023  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 		if [ "$verbosity" -gt 0 ]; then
     44 			printf "Created: %s\n" "$3"
     45 		fi
     46 	elif [ "$2" = "post" ]; then
     47 		{ printf "Title: \nAuthor: \nDate: \nSummary: \n\n"
     48 		  printf "# [%%title]\n\n"
     49 		  printf "**Author:** [%%author] | **Published:** [%%date]\n\n"
     50 		} > "$3"
     51 		if [ "$verbosity" -gt 0 ]; then
     52 			printf "Created: %s\n" "$3"
     53 		fi
     54 	elif [ "$2" = "site" ]; then
     55 		mkdir "$3" "$3/content/" "$3/static/" "$3/templates/"
     56 		# Create config.ini
     57 		{ printf "siteURL = https://example.com/\n"
     58 		  printf "siteName = %s\n" "$3"
     59 		  printf "blogDir = blog/\n"
     60 		  printf "languageCode = en\n"
     61 		  printf "buildOptions = -Thtml --html-no-skiphtml --html-no-escapehtml\n"
     62 		  printf "pushCommand = echo 'No command configured.'\n"
     63 		} > "$3/config.ini"
     64 		# Create template header.html file
     65 		{ printf '<!DOCTYPE html>\n'
     66 		  printf '<html lang="">\n'
     67 		  printf '<head>\n'
     68 		  printf '\t<meta charset="utf-8">\n'
     69 		  printf '\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
     70 		  printf '\t<meta name="description" content="">\n'
     71 		  printf '\t<link rel="stylesheet" href="/style.css">\n'
     72 		  printf '\t<link rel="alternate" type="application/rss+xml" title="RSS feed" href="/feed.xml">\n'
     73 		  printf '\t<title></title>\n'
     74 		  printf '</head>\n'
     75 		  printf '<body>\n'
     76 		  printf '\t<header></header>\n'
     77 		  printf '\t<main>\n'
     78 		} > "$3/templates/header.html"
     79 		# Create template footer.html file
     80 		{ printf '\t</main>\n'
     81 		  printf '\t<footer></footer>\n'
     82 		  printf '</body>\n'
     83 		  printf '</html>\n'
     84 		} > "$3/templates/footer.html"
     85 		# Create style.css
     86 		{ printf "html {\n"
     87 		  printf "\tmax-width: 38em;\n"
     88 		  printf "\tpadding: 1em;\n"
     89 		  printf "\tmargin: auto;\n"
     90 		  printf "\tline-height: 1.4em;\n"
     91 		  printf "}\n\n"
     92 		  printf "img {\n"
     93 		  printf "\tmax-width: 100%%;\n"
     94 		  printf "}\n"
     95 		} > "$3/static/style.css"
     96 		if [ "$verbosity" -gt 0 ]; then
     97 			printf "Created: %s\n" "$3"
     98 		fi
     99 	else
    100 		printf "Subcommand '%s' not recognized. See sbs(1) for documentation.\n" "$2"
    101 		exit 1
    102 	fi
    103 	exit 0
    104 }
    105 
    106 parse_configuration()
    107 {
    108 	options="siteURL siteName blogDir languageCode buildOptions pushCommand"
    109 	for key in $options; do
    110 		value=$(grep "$key" config.ini | cut -d'=' -f2 | xargs)
    111 		if [ -n "$value" ]; then
    112 			eval "$key='$value'"
    113 		else
    114 			printf "Error: %s is not configured.\n" "$key"
    115 			exit 1
    116 		fi
    117 	done
    118 
    119 	# Validate configuration
    120 	if ! echo "$siteURL" | grep -qE '^https?://.*\..*/$'; then
    121 		echo "Error: siteURL should be in canonical form (e.g. https://example.com/).\n"
    122 		exit 1
    123 	fi
    124 }
    125 
    126 # Construct a complete atom feed from all blog posts
    127 genfeed()
    128 {
    129 	{ printf '<?xml version="1.0" encoding="utf-8"?>\n'
    130 	  printf '<feed xmlns="http://www.w3.org/2005/Atom">\n'
    131 	  printf "\t<title>%s</title>\n" "$siteName"
    132 	  printf "\t<link href=\"%s\" />\n" "$siteURL"
    133 	  printf "\t<link rel=\"self\" href=\"%sfeed.xml\" />\n" "${siteURL}"
    134 	  printf "\t<icon>/favicon.png</icon>\n"
    135 	  printf "\t<updated>%s</updated>\n" "$(date +"%Y-%m-%dT%H:%M:%S%z")"
    136 	  printf "\t<id>%s</id>\n" "$siteURL"
    137 	  printf "\t<generator>sbs</generator>\n\n"
    138 	} > static/feed.xml
    139 
    140 	tmp=$(mktemp)
    141 	find content/"$blogDir" -type f -name '*.md' -not -name index.md | while read -r file; do
    142 		if [ -n "$(lowdown -X draft "$file" 2>/dev/null)" ]; then
    143 			continue
    144 		fi
    145 		printf "%s %s\n" "$(lowdown -X date "$file")" "$file" >> "$tmp"
    146 	done
    147 	sort -rn "$tmp" | cut -d' ' -f2 | while read -r file; do
    148 		fileName=$(basename "$file" .md).html
    149 		subDir=$(dirname "$file" | sed "s/content\///")
    150 
    151 		title=$(lowdown -X title "$file")
    152 		author=$(lowdown -X author "$file")
    153 		date=$(lowdown -X date "$file")
    154 
    155 		{ printf "\t<entry>\n"
    156 		  printf "\t\t<title>%s</title>\n" "$title"
    157 		  printf "\t\t<author><name>%s</name></author>\n" "$author"
    158 		  printf "\t\t<link href=\"%s%s/%s\" />\n" "$siteURL" "$subDir" "$fileName"
    159 		  printf "\t\t<id>%s%s/%s</id>\n" "$siteURL" "$subDir" "$fileName"
    160 		  printf "\t\t<updated>%s</updated>\n" "${date}T00:00:00$(date +%z)"
    161 		  printf "\t\t<content type=\"html\"><![CDATA[\n%s\n\t\t]]></content>\n" "$(lowdown $buildOptions "$file")"
    162 		  printf "\t</entry>\n\n"
    163 		} >> static/feed.xml
    164 	done
    165 
    166 	numEntries="$(wc -l "$tmp" | awk '{print $1}')"
    167 	printf '</feed>\n' >> static/feed.xml
    168 	if [ "$verbosity" -gt 0 ]; then
    169 		printf "Created: static/feed.xml with %s entries.\n" "$numEntries"
    170 	fi
    171 	rm "$tmp"
    172 	exit 0
    173 }
    174 
    175 # Build the pages given as arguments
    176 build()
    177 {
    178 	for file in "$@"; do
    179 		unset title
    180 		unset description
    181 		# Stop the filename from being prepended with the path multiple
    182 		# times as build() recurses
    183 		if ! echo "$file" | grep -q "$cwd"; then
    184 			file="$cwd"/"$file"
    185 		fi
    186 		if [ ! -f "$file" ]; then
    187 			if [ -d "$file" ]; then
    188 				build "$file"/*
    189 				continue
    190 			fi
    191 			printf "Error: %s does not exist. " "$file"
    192 			printf "Are you sure you're in the right directory?\n"
    193 			exit 1
    194 		fi
    195 
    196 		fileName=$(basename "$file" .md)
    197 		subDir=$(dirname "$file" | sed "s/.*\/content//")
    198 		mkdir -p "static/$subDir"
    199 
    200 		if [ "$verbosity" -gt 1 ]; then
    201 			printf "Creating: static%s/%s.html...\n" "$subDir" "$fileName"
    202 		fi
    203 
    204 		# Extract metadata from markdown doc (if not converted from gmi)
    205 		title=${title:-$(lowdown -X title "$file")}
    206 		description=${description:-$(lowdown -X summary "$file")}
    207 
    208 		# Escapes characters from text that might interfere with sed
    209 		title=$(echo $title | sed 's/\\/\\\\/g; s/\//\\\//g; s/\^/\\^/g;
    210 		s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')
    211 		description=$(echo $description | sed 's/\\/\\\\/g; s/\//\\\//g;
    212 		s/\^/\\^/g; s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')
    213 
    214 		# Build and process the output document
    215 		lowdown $buildOptions "$file" \
    216 			| cat "templates/header.html" - "templates/footer.html" \
    217 			| sed -e "s/<title><\/title>/<title>$title - $siteName<\/title>/" \
    218 			-e "s/lang=\"\"/lang=\"$languageCode\"/" \
    219 			-e "s/content=\"\"/content=\"$description\"/" \
    220 			> "static/$subDir/$fileName".html
    221 		count=$(($count + 1))
    222 	done
    223 }
    224 
    225 # Push the contents of the static/ folder using the configured command
    226 push()
    227 {
    228 	if [ "$verbosity" -gt 0 ]; then
    229 		echo "$pushCommand"
    230 	fi
    231 	sh -c "$pushCommand"
    232 }
    233 
    234 # Walks up the filesystem to the root of the website so it can be built from
    235 # within any subdirectory. Has the side effect of making path-parsing more
    236 # resilient.
    237 walk_back()
    238 {
    239 	# config.ini should be in the root of the website's folder structure
    240 	while [ ! -f config.ini ]; do
    241 		cd ..
    242 		if [ $(pwd) = "/" ]; then
    243 			printf "Error: Not inside of an sbs site directory.\n"
    244 			exit 1
    245 		fi
    246 	done
    247 
    248 	# Parse the config just for the siteName variable to ensure we're in the
    249 	# right place (in the root of the website's folder structure)
    250 	value=$(grep "siteName" config.ini | cut -d'=' -f2 | xargs)
    251 	if [ -n "$value" ]; then
    252 		eval "siteName='$value'"
    253 	else
    254 		printf "Error: siteName is not configured.\n"
    255 		exit 1
    256 	fi
    257 
    258 	# Check that we are in the root of the website's folder structure
    259 	if [ "$(basename $(pwd))" = "$siteName" ]; then
    260 		return 0
    261 	else
    262 		printf "Error: config.ini found but %s is not the root of the site.\n" "$(pwd)"
    263 		exit 1
    264 	fi
    265 }
    266 
    267 verbosity=0
    268 
    269 if [ "$#" -gt 1 ]; then
    270 	case "$1" in
    271 		"-v" )
    272 			verbosity=1
    273 			shift
    274 			;;
    275 		"-v"* )
    276 			verbosity=2
    277 			shift
    278 			;;
    279 	esac
    280 fi
    281 
    282 if [ "$#" -lt 1 ]; then
    283 	echo "Please provide a command. See sbs(1) for documentation."
    284 	exit 1
    285 fi
    286 
    287 case "$1" in
    288 	"build")
    289 		shift
    290 		# Store the current directory so we know where we started
    291 		cwd="$(pwd)"
    292 		walk_back
    293 		parse_configuration
    294 		# Allows simply running "sbs build" without path(s)
    295 		count=0
    296 		start_s=$(date +%s)
    297 		if [ $# -eq 0 ]; then
    298 			cwd=""
    299 			build ./content/*
    300 		else
    301 			build "$@"
    302 		fi
    303 		end_s=$(date +%s)
    304 		if [ "$verbosity" -gt 0 ]; then
    305 			printf "Built %d files in %d seconds.\n" \
    306 				$count "$(($end_s - $start_s))"
    307 		fi
    308 		;;
    309 	"genfeed")
    310 		walk_back
    311 		parse_configuration
    312 		genfeed
    313 		;;
    314 	"new")
    315 		new "$@"
    316 		;;
    317 	"push")
    318 		walk_back
    319 		parse_configuration
    320 		push
    321 		;;
    322 	"version")
    323 		echo "v1.1.3" ;
    324 		;;
    325 	*)
    326 		echo "Unknown command. See sbs(1) for documentation."
    327 		;;
    328 esac
    329 
    330 exit 0