From 24e24b49fb6375189cfa527a113eb965087a293a Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Wed, 5 Apr 2023 16:14:00 -0300 Subject: v2: Support Atom feeds and collection translations --- v2/src/bin/absolute | 2 +- v2/src/bin/collections | 69 +++++++++++++++ v2/src/bin/conf | 166 ++++++++++++++++++++++++++++++++++++ v2/src/bin/feed | 91 ++++++++++++++++++++ v2/src/bin/html | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ v2/src/bin/lang-for | 73 ++++++++++++++++ v2/src/bin/langs | 69 +++++++++++++++ v2/src/bin/makemake | 142 ++++++++++++++++++------------- v2/src/bin/security-txt | 82 ++++++++++++++++++ v2/src/bin/url-for | 30 +++---- v2/src/bin/xmlentry | 81 ++++++++++++++++++ 11 files changed, 946 insertions(+), 78 deletions(-) create mode 100755 v2/src/bin/collections create mode 100755 v2/src/bin/conf create mode 100755 v2/src/bin/feed create mode 100755 v2/src/bin/html create mode 100755 v2/src/bin/lang-for create mode 100755 v2/src/bin/langs create mode 100755 v2/src/bin/security-txt create mode 100755 v2/src/bin/xmlentry (limited to 'v2/src/bin') diff --git a/v2/src/bin/absolute b/v2/src/bin/absolute index 6434219..ecf5a64 100755 --- a/v2/src/bin/absolute +++ b/v2/src/bin/absolute @@ -62,6 +62,6 @@ done shift $((OPTIND - 1)) -. src/lib/base-conf +. src/lib/base.conf printf 'https://%s%s' "$domain" "$(cat)" diff --git a/v2/src/bin/collections b/v2/src/bin/collections new file mode 100755 index 0000000..1df0c39 --- /dev/null +++ b/v2/src/bin/collections @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + collections + collections -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + + List the registered collections to STDOUT. + + This is done by emiting the content of the "$COLLECTIONS" + environment variable. + + + Examples: + + Just run it: + + $ collections + tils + pastebins + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + esac +done +shift $((OPTIND - 1)) + + +echo "$COLLECTIONS" | + tr ' ' '\n' | + grep . diff --git a/v2/src/bin/conf b/v2/src/bin/conf new file mode 100755 index 0000000..ac02d98 --- /dev/null +++ b/v2/src/bin/conf @@ -0,0 +1,166 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + conf FILENAME + conf -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + FILENAME the name of the input file, also to be used as + URL. + + + Separate the content from the "frontmatter", and emit the + selected one, given the FILENAME. + + + Examples: + + Get the "frontmatter" of src/f.conf: + + $ conf src/f.md > src/f.conf + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +. src/lib.sh + +FILENAME="${1:-}" +eval "$(assert_arg "$FILENAME" 'FILENAME')" + + +escape() { + sed 's|\([`"$]\)|\\\1|g' +} + +tee "$FILENAME".tmp < src/lib/base.conf +DELIMITER=0 +while read -r line; do + if [ "$line" = '---' ]; then + DELIMITER=$((DELIMITER + 1)) + continue + fi + if [ "$DELIMITER" = 2 ]; then + break + fi + if [ -z "$line" ]; then + continue + fi + + KEY="$( printf '%s' "$line" | cut -d: -f1)" + VALUE="$(printf '%s' "$line" | cut -d: -f2- | sed 's|^ ||' | escape)" + printf 'export %s="%s"\n' "$KEY" "$VALUE" +done < "$FILENAME" | tee -a "$FILENAME".tmp +# shellcheck source=/dev/null +. "$FILENAME".tmp +rm -f "$FILENAME".tmp + + +lang="$(lang-for "$FILENAME")" +export lang +printf 'export lang="%s"\n' "$lang" + +cat src/lib/base."${lang:?}".conf +# shellcheck source=/dev/null +. src/lib/base."$lang".conf +if [ -z "${title:-}" ]; then + title="${site_name:?}" + printf 'export title="%s"\n' "$(printf '%s' "$title" | escape)" +fi + +if [ -n "${date:-}" ]; then + date_iso="$(date -ud "${date:?}" -Is)" + printf 'export date_iso="%s"\n' "$date_iso" + + formatted_date="$(LANG="$lang" date -ud "${date:?}" +"${date_fmt:?}")" + export formatted_date + printf 'export date_html="%s"\n' "$(envsubst < src/lib/date."$lang".html | escape)" + + echo "${FILENAME%.md}.xmlentry" > "$(dirname "$FILENAME")/$date_iso.sortdata" + touch "${FILENAME%.md}.sortref" +fi + +if [ -n "${update:-}" ]; then + update_iso="$(date -ud "${update:?}" -Is)" + printf 'export update_iso="%s"\n' "$update_iso" + + formatted_update="$(LANG="$lang" date -ud "${update:?}" +"${date_fmt:?}")" + export formatted_update + printf 'export update_html="%s"\n' "$(envsubst < src/lib/update."$lang".html | escape)" + + printf 'export update_xml=" %s"\n' "$update_iso" +fi + + +url_part="$(printf '%s' "${FILENAME%.md}.html" | sed "s|^$CONTENT_PREFIX||")" +title_uri="$(uri "$title")" + +printf 'export title_html="%s"\n' "$(printf '%s' "$title" | htmlesc | escape)" +printf 'export filename="%s"\n' "$FILENAME" +printf 'export url_part="%s"\n' "$url_part" +printf 'export url="%s"\n' "$(url-for "$url_part" | absolute)" +printf 'export mailto_uri="%s%s"\n' "${mailto_uri_prefix:?}" "$title_uri" +printf 'export discussions_url="%s%s"\n' "${discussions_url_prefix:?}" "$title_uri" +printf 'export sourcecode_url="%s%s"\n' "${sourcecode_url_prefix:?}" "$FILENAME" + +printf 'export style_url="%s"\n' "$(url-for 'style.css')" +printf 'export favicon_url="%s"\n' "$(url-for 'favicon.svg')" +printf 'export pubkey_url="%s"\n' "$(url-for 'public.asc.txt')" + +for f in "$CONTENT_PREFIX"/img/*.svg; do + name="$(basename "$f" .svg | sed 's|-|_|g')" + printf 'export icon_%s_url="%s"\n' "$name" "$(url-for "img/$(basename "$f")")" +done + +# FIXME: special treatment of root +printf 'export homepage_url="%s"\n' "$(url-for '/')" + +printf 'export about_url="%s"\n' "$(url-for "${about_url_name:?}")" + + +if [ "${layout:-}" = 'post' ]; then + export mailto_uri="$mailto_uri_prefix$title_uri" + export discussions_url="$discussions_url_prefix$title_uri" + export sourcecode_url="$sourcecode_url_prefix$FILENAME" + printf 'export comment_html="%s"\n' "$(envsubst < src/lib/comment."$lang".html | escape)" +fi diff --git a/v2/src/bin/feed b/v2/src/bin/feed new file mode 100755 index 0000000..96c40a6 --- /dev/null +++ b/v2/src/bin/feed @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + feed FILENAME + feed -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + FILENAME the target feed to be generated + + + Generate FILENAME as an Atom feed. The collection type and + language are inferred by the name of FILENAME. + + + Examples: + + Generate a feed for TILs: + + $ feed src/en/feeds/til.xml + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +. src/lib.sh + +FILENAME="${1:-}" +eval "$(assert_arg "$FILENAME" 'FILENAME')" + + +COLLECTION="$(basename "$FILENAME" '.xml')" +LANGUAGE="$(lang-for "$FILENAME")" +DIR="$(dirname "$(dirname "$FILENAME")")/$COLLECTION" + + +. src/lib/base.conf +# shellcheck source=/dev/null +. src/lib/base."$LANGUAGE".conf + +now="$(date -uIs)" +url="$(url-for "${FILENAME#"$CONTENT_PREFIX"}" | absolute)" +site_name_html="$(htmlesc "${site_name:?}")" +export now url site_name_html + + +mkdir -p "$(dirname "$FILENAME")" +{ + envsubst < src/lib/feed.xml + find "$DIR"/*.sortdata | sort -nr | xargs cat | xargs cat + printf '\n' +} > "$FILENAME" diff --git a/v2/src/bin/html b/v2/src/bin/html new file mode 100755 index 0000000..7e6809c --- /dev/null +++ b/v2/src/bin/html @@ -0,0 +1,219 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + html FILENAME + html -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + FILENAME the name of the input file .md file + + + Process the FILENAME, and generate a full HTML page. + + + Examples: + + Generate the HTML for a pastebin: + + $ html src/a-paste.md > src/a-paste.html + EOF +} + + +for f in "$@"; do + case "$f" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +. src/lib.sh + +FILENAME="${1:-}" +eval "$(assert_arg "$FILENAME" 'FILENAME')" + + +# shellcheck source=/dev/null +. "${FILENAME%.md}.conf" + +# +# Utility functions +# + +SEEN_SLUGS="$(mkstemp)" +slugify_once() { + SLUG="$(printf '%s' "$1" | slugify)${2:+-$2}" + if grep -q "^$SLUG$" "$SEEN_SLUGS"; then + N="${2:-0}" + N=$((N + 1)) + slugify_once "$1" "$N" + else + printf '%s\n' "$SLUG" >> "$SEEN_SLUGS" + printf '%s' "$SLUG" + fi +} + +INDENT=' ' +markdown_to_html() { + md2html | awk -vINDENT="$INDENT" ' + BEGIN { + in_block = 0 + } + + { + if (in_block == 0) { + printf "%s", INDENT + } + print + } + + /^<\/code><\/pre>$/ { + in_block = 0 + } + + /^
 "$SNIPPETS"
+	F="$(mkstemp)"
+	cat > "$F"
+	(
+		IFS=''
+		BLOCK_NUMBER=0
+		IN_BLOCK=
+		while read -r line; do
+			if [ "$line" = '
' ]; then + IN_BLOCK= + fi + + if [ -n "$IN_BLOCK" ]; then + printf '%s\n' "$line" | htmlesc -d >> "$OUT" + fi + + if printf '%s' "$line" | grep -q "^$INDENT
\)\(.*\)$|\2|" |
+					htmlesc -d > "$OUT"
+				printf '%s\n' "$OUT" >> "$SNIPPETS"
+			fi
+		done < "$F"
+
+		BLOCK_NUMBER=0
+		while read -r line; do
+			printf '%s\n' "$line"
+
+			if [ "$line" = '
' ]; then + printf '%s\n' \ + "$INDENT" \ + "$(basename "${url_part:?}").$BLOCK_NUMBER.txt" + BLOCK_NUMBER=$((BLOCK_NUMBER + 1)) + fi + done < "$F" + ) + +} + +add_line_numbers() { + awk ' + /^<\/code><\/pre>$/ { + in_block = 0 + printf "%s\n", $0 + next + } + + match($0, /^( +
)(.*)$/, a) {
+			printf "%s", a[1]
+
+			n = 1
+			block_count++
+			printf "\n", block_count, n, block_count, n, n, a[2]
+			in_block = 1
+			next
+		}
+
+		in_block == 1 {
+			n++
+			printf "\n", block_count, n, block_count, n, n, $0
+			next
+		}
+
+		{ print }
+	'
+}
+
+add_headings_anchors() {
+	(
+		IFS=''
+		while read -r line; do
+			if ! printf '%s' "$line" | grep -q "^$INDENT"; then
+				printf '%s\n' "$line"
+				continue
+			fi
+			LVL="$(printf '%s' "$line" | sed "s|^$INDENT.*|\1|")"
+			HEADING="$(printf '%s' "$line" | sed "s|^$INDENT\(.*\)$|\1|")"
+			SLUG="$(slugify_once "$HEADING")"
+			printf '%s%s\n' \
+				"$INDENT"  \
+				"$LVL"     \
+				"$SLUG"    \
+				"$HEADING" \
+				"$SLUG"    \
+				"${icon_link_url:?}" \
+				"$LVL"
+		done
+	)
+}
+
+emit_body() {
+	< "${FILENAME%.md}.content" \
+		markdown_to_html           |
+		extract_plaintext_snippets |
+		add_line_numbers           |
+		add_headings_anchors
+}
+
+envsubst < src/lib/preamble.html
+emit_body | tee "${FILENAME%.md}.htmlbody"
+envsubst < src/lib/postamble.html
diff --git a/v2/src/bin/lang-for b/v2/src/bin/lang-for
new file mode 100755
index 0000000..f7c57a9
--- /dev/null
+++ b/v2/src/bin/lang-for
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+usage() {
+	cat <<-'EOF'
+		Usage:
+		  lang-for FILE
+		  lang-for -h
+	EOF
+}
+
+help() {
+	cat <<-'EOF'
+
+
+		Options:
+		  -h, --help    show this message
+
+		  FILE          the path of the file to get the language for
+
+
+		Say the language of the given file, using the path of FILE.
+
+
+		Examples:
+
+		  Get "en" for "src/en/some-pt.md":
+
+		    $ lang-for src/en/some-pt.md
+		    en
+	EOF
+}
+
+
+for flag in "$@"; do
+	case "$flag" in
+		--)
+			break
+			;;
+		--help)
+			usage
+			help
+			exit
+			;;
+		*)
+			;;
+	esac
+done
+
+while getopts 'h' flag; do
+	case "$flag" in
+		h)
+			usage
+			help
+			exit
+			;;
+		*)
+			usage >&2
+			exit 2
+			;;
+	esac
+done
+shift $((OPTIND - 1))
+
+. src/lib.sh
+
+
+FILE="${1:-}"
+eval "$(assert_arg "$FILE" 'FILE')"
+
+
+echo "${FILE#"$CONTENT_PREFIX"/}" |
+	cut -d/ -f1
diff --git a/v2/src/bin/langs b/v2/src/bin/langs
new file mode 100755
index 0000000..8e8aa63
--- /dev/null
+++ b/v2/src/bin/langs
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+
+usage() {
+	cat <<-'EOF'
+		Usage:
+		  langs
+		  langs -h
+	EOF
+}
+
+help() {
+	cat <<-'EOF'
+
+
+		Options:
+		  -h, --help    show this message
+
+
+		List the supported languages, including english, to STDOUT.
+
+		This is done in the same way that po4a(1) does it: by listing
+		the po/*.po files, and getting the name from it.
+
+
+		Examples:
+
+		  Just run it:
+
+		    $ langs
+		    en
+		    pt
+	EOF
+}
+
+
+for flag in "$@"; do
+	case "$flag" in
+		--)
+			break
+			;;
+		--help)
+			usage
+			help
+			exit
+			;;
+		*)
+			;;
+	esac
+done
+
+while getopts 'h' flag; do
+	case "$flag" in
+		h)
+			usage
+			help
+			exit
+			;;
+		*)
+			usage >&2
+			exit 2
+	esac
+done
+shift $((OPTIND - 1))
+
+
+echo po/en.po po/*.po |
+	tr ' ' '\n' |
+	sed 's|^po/\(.*\)\.po$|\1|'
diff --git a/v2/src/bin/makemake b/v2/src/bin/makemake
index e95a040..fdd0e68 100755
--- a/v2/src/bin/makemake
+++ b/v2/src/bin/makemake
@@ -64,84 +64,110 @@ varlist() {
 	printf '\n'
 }
 
-html_deps() {
-	"$@" | sed 's/^\(.*\)\.md$/\1.conf \1.content: \1.md/'
-	"$@" | sed 's/^\(.*\)\.md$/\1.snippets \1.html: \1.conf \1.content/'
-	printf '\n'
-}
-
-
-content_mds() {
-	{
-		if [ "$r" = 'pages' ]; then
-			echo src/content/*.md
-		else
-			echo src/content/"$r"/*.md
-		fi
-	} | tr ' ' '\n'
-}
-
-RESOURCES='
-pages
-pastebins
-tils
-'
 
 EXTENSIONS='
-.md
 .conf
 .content
 .html
 .snippets
+.htmlbody
 '
 
-for r in $RESOURCES; do
-	content_mds "$r" | varlist "$r".md
-	for e in $EXTENSIONS; do
-		if [ "$e" = '.md' ]; then
-			continue
+COLL_EXTENSIONS='
+.sortref
+.xmlentry
+'
+
+page_ext_filter="^($(echo "$COLL_EXTENSIONS" |
+	tr ' ' '\n' |
+	grep . |
+	paste -sd'|'
+))\$"
+
+
+extensions() {
+	echo "$EXTENSIONS" "$COLL_EXTENSIONS" "$@" |
+		tr ' ' '\n' |
+		grep .
+}
+
+
+printf '.POSIX:\n\n\n'
+for lang in $(langs); do
+	for c in pages $(collections); do
+		if [ "$c" = 'pages' ]; then
+			filter="$page_ext_filter"
+			dir="$CONTENT_PREFIX/$lang"
+		else
+			filter='^$'
+			dir="$CONTENT_PREFIX/$lang/$c"
 		fi
-		# shellcheck disable=2016
-		printf '%s%s = $(%s.md:.md=%s)\n' "$r" "$e" "$r" "$e"
-	done
-	printf '%s =' "$r"
-	for e in $EXTENSIONS; do
-		if [ "$e" = '.md' ]; then
+
+		mds() {
+			# find "$dir"/*.md # FIXME
+			find "$dir"/*.md 2>/dev/null
+		}
+
+		exts() {
+			extensions | grep -Ev "$filter"
+		}
+
+		mds | varlist "$c.$lang.md"
+		exts | sed "s|^\(.*\)\$|$c.$lang\1 = \$($c.$lang.md:.md=\1)|"
+		exts | sed "s|^\(.*\)\$|\$($c.$lang\1)|" | varlist "$c.$lang"
+
+		mds | sed 's/^\(.*\)\.md$/\1.conf \1.content: \1.md/'
+		mds | sed 's/^\(.*\)\.md$/\1.snippets \1.htmlbody \1.html: \1.conf \1.content/'
+		if [ "$c" = 'pages' ]; then
 			continue
 		fi
-		# shellcheck disable=2016
-		printf ' $(%s%s)' "$r" "$e"
+
+		mds | sed 's/^\(.*\)\.md$/\1.sortref: \1.md/'
+		mds | sed 's/^\(.*\)\.md$/\1.xmlentry: \1.html/'
+
+		echo "$CONTENT_PREFIX/$lang/feeds/$c.xml: \$($c.$lang.xmlentry)"
+
+		printf '\n\n'
 	done
-	printf '\n'
-	html_deps content_mds "$r"
-	printf '\n'
-done
 
+	for e in $(extensions); do
+		{
+			if ! printf '%s\n' "$e" | grep -qE "$page_ext_filter"; then
+				echo pages
+			fi
+			collections
+		} |
+		sed "s|^\(.*\)\$|\$(\1.$lang$e)|" |
+		varlist "all-generated.$lang$e"
+	done
 
-all_resources() {
-	echo "$RESOURCES" | tr ' ' '\n' | grep .
-}
+	collections |
+		sed "s|^\(.*\)\$|$CONTENT_PREFIX/$lang/feeds/\1.xml|" |
+		varlist "all-generated.$lang.xml"
 
-all_vars() {
-	EXT="$1"
-	# shellcheck disable=2016
-	all_resources |
-		sed 's|^|$(|' |
-		sed "s|$|$EXT)|" |
-		varlist all-generated"$EXT"
-}
+	extensions '.xml' |
+		sed "s|^\(.*\)\$|\$(all-generated.$lang\1)|" |
+		varlist "all-generated.$lang"
 
-for e in $EXTENSIONS; do
-	all_vars "$e"
+	printf '\n'
+done
+
+for e in $(extensions .xml); do
+	langs |
+		sed "s|^\(.*\)\$|\$(all-generated.\1$e)|" |
+		varlist "all-generated$e"
 done
-all_vars ''
+
+# shellcheck disable=2016
+langs |
+	sed 's|^\(.*\)$|$(all-generated.\1)|' |
+	varlist 'all-generated'
 
 
-git ls-files |
-	grep -v '^src/content/' |
+git ls-files src/ |
+	grep -v ^"$CONTENT_PREFIX"/ |
 	varlist 'non-content'
 
-git ls-files src/content/ |
+git ls-files "$CONTENT_PREFIX"/ |
 	grep -v '\.md$' |
-	grep -Ev "src/content/($(all_resources | paste -sd'|'))/" |
 	varlist 'static-content'
diff --git a/v2/src/bin/security-txt b/v2/src/bin/security-txt
new file mode 100755
index 0000000..7026969
--- /dev/null
+++ b/v2/src/bin/security-txt
@@ -0,0 +1,82 @@
+#!/bin/sh
+set -eu
+
+usage() {
+	cat <<-'EOF'
+		Usage:
+		  security-txt
+		  security-txt -h
+	EOF
+}
+
+help() {
+	cat <<-'EOF'
+
+
+		Options:
+		  -h, --help    show this message
+
+
+		Generate the RFC 9116 "security.txt" file from data in the
+		repository.
+
+
+		Examples:
+
+		  Just run it:
+
+		    $ security-txt > .well-known/security.txt
+	EOF
+}
+
+
+for flag in "$@"; do
+	case "$flag" in
+		--)
+			break
+			;;
+		--help)
+			usage
+			help
+			exit
+			;;
+		*)
+			;;
+	esac
+done
+
+while getopts 'h' flag; do
+	case "$flag" in
+		h)
+			usage
+			help
+			exit
+			;;
+		*)
+			usage >&2
+			exit 2
+	esac
+done
+shift $((OPTIND - 1))
+
+
+
+EXPIRES="$(
+	LANG=C.UTF-8 gpg --list-key "$EMAIL" |
+		awk '/^pub/ { print substr($(NF), 1, 10) }'
+)T00:00:00z"
+
+LANGUAGES="$(
+	langs |
+		sed 's|^|, |' |
+		tr -d '\n' |
+		sed 's|^, ||'
+)"
+
+
+cat <<-EOF
+	Contact: mailto:$EMAIL
+	Encryption: $(url-for 'public.asc.txt' | absolute)
+	Expires: $EXPIRES
+	Preferred-Languages: $LANGUAGES
+EOF
diff --git a/v2/src/bin/url-for b/v2/src/bin/url-for
index d4099bc..adaccd7 100755
--- a/v2/src/bin/url-for
+++ b/v2/src/bin/url-for
@@ -4,7 +4,7 @@ set -eu
 usage() {
 	cat <<-'EOF'
 		Usage:
-		  url-for [-g] FILE
+		  url-for FILE
 		  url-for -h
 	EOF
 }
@@ -13,7 +13,6 @@ help() {
 	cat <<-'EOF'
 
 		Options:
-		  -g            global file, not specific to a single language
 		  -h, --help    show this message
 
 		  FILE          the path for the URL to be constructed
@@ -25,7 +24,7 @@ help() {
 
 		Examples:
 
-		  Get the URL for "about.html", when $base_url is "v2" and $lang is "en":
+		  Get the URL for "en/about.html", when $base_url is "v2":
 
 		    $ url-for 'about.html'
 		    /v2/en/about.html
@@ -33,8 +32,8 @@ help() {
 
 		  Get the URL for "static/favicon.svg", when $base_url is empty:
 
-		    $ url-for -g 'static/favicon.svg'
-		    /static/favicon.svg
+		    $ url-for 'img/link.svg'
+		    /img/link.svg
 	EOF
 }
 
@@ -54,12 +53,8 @@ for flag in "$@"; do
 	esac
 done
 
-GLOBAL=false
-while getopts 'gh' flag; do
+while getopts 'h' flag; do
 	case "$flag" in
-		g)
-			GLOBAL=true
-			;;
 		h)
 			usage
 			help
@@ -73,22 +68,19 @@ while getopts 'gh' flag; do
 done
 shift $((OPTIND - 1))
 
-. src/development/lib.sh
-
+. src/lib.sh
 
 FILE="${1:-}"
+eval "$(assert_arg "$FILE" 'FILE')"
+
+
 if [ "$FILE" = '/' ]; then
 	FILE=''
 fi
 
-. src/lib/base-conf
+. src/lib/base.conf
 # shellcheck source=/dev/null
 . src/lib/base."${lang:?}".conf
 
-if [ "$GLOBAL" = true ]; then
-	L=''
-else
-	L="${lang:?}/"
-fi
 
-printf '%s%s%s' "${base_url:-/}" "$L" "$FILE"
+printf '%s%s' "${base_url:-/}" "$FILE"
diff --git a/v2/src/bin/xmlentry b/v2/src/bin/xmlentry
new file mode 100755
index 0000000..b0760ae
--- /dev/null
+++ b/v2/src/bin/xmlentry
@@ -0,0 +1,81 @@
+#!/bin/sh
+set -eu
+
+usage() {
+	cat <<-'EOF'
+		Usage:
+		  xmlentry FILENAME
+		  xmlentry -h
+	EOF
+}
+
+help() {
+	cat <<-'EOF'
+
+
+		Options:
+		  -h, --help    show this message
+
+		  FILENAME      the name of the input .md file
+
+
+		Process FILE, and generate an Atom feed entry.
+
+
+		Examples:
+
+		  Generate the XML entry for a TIL:
+
+		    $ xmlentry src/tils/a-til.md > src/tils/a-til.xml
+	EOF
+}
+
+
+for flag in "$@"; do
+	case "$flag" in
+		--)
+			break
+			;;
+		--help)
+			usage
+			help
+			exit
+			;;
+		*)
+			;;
+	esac
+done
+
+while getopts 'h' flag; do
+	case "$flag" in
+		h)
+			usage
+			help
+			exit
+			;;
+		*)
+			usage >&2
+			exit 2
+			;;
+	esac
+done
+shift $((OPTIND - 1))
+
+. src/lib.sh
+
+FILENAME="${1:-}"
+eval "$(assert_arg "$FILENAME" 'FILENAME')"
+
+
+# shellcheck source=/dev/null
+. "${FILENAME%.md}.conf"
+
+envsubst < src/lib/entry.xml
+
+head -n1 < "${FILENAME%.md}.htmlbody" | htmlesc
+printf '    \n'
+printf '    \n' "${url:?}"
+
+htmlesc < "${FILENAME%.md}.htmlbody"
+printf '    \n'
+printf '  \n'
-- 
cgit v1.2.3

%s%s
%s%s