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