~jbauer/sbs

ref: 44f89f89c0f38328c44dd52bb1c1b4ec4846c580 sbs/sbs -rwxr-xr-x 9.0 KiB
44f89f89Jake Bauer Add exclusion for index page in feed generation 4 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#!/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' -not -name index.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")}

		# Escapes characters from text that might interfere with sed
		title=$(echo $title | sed 's/\\/\\\\/g; s/\//\\\//g; s/\^/\\^/g;
		s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')
		description=$(echo $description | sed 's/\\/\\\\/g; s/\//\\\//g;
		s/\^/\\^/g; s/\[/\\[/g; s/\*/\\*/g; s/\./\\./g; s/\$/\\$/g')

		# 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.7.0" ;
		;;
	*)
		echo "Usage: sbs <command> [FILE ...]"
		;;
esac

exit 0