#!/bin/bash

################################################################################################################################################################
#
# SBWG - sweet bash website generator
# Last changed: 2022-07-10
# Version: See variable below
# https://log.steeph.de/SBWG.html
#
# You can place this file in a directory in your $PATH or execute it from anywhere. It works independently from the web site's source directory.
#
# Small overview of some functions and variables at the bottom of the script file.
#
################################################################################################################################################################

#set -o errexit									# Exit in the account of an error
#set -o nounset									# Have a little error when an unset variable is attempted to be used.
#set -o pipefail								# From lines with pipes exit with the exit status of the last command that threw a
										#   non-zero exit code.
shopt -s extglob								# Set extended globbing. Needed for various things and stuff, can't be in a function.
shopt -s globstar								# Set globstar. Needed for looping through source files io multiple directory levels.
set +o histexpand								# Disable history expansion because strings are not handled safely in this script.
histchars=									# Same effect as the line above. Not sure which one I should/want to use.
FUNCNEST=100									# If that limit is reached we're probably in some endless loop. Might as well quit.
#tmpdir="${TMPDIR:-/tmp}/sbwg"							# Set fallback of temporary directory in case something happens before set properly. #todo: is this dir created if it doesn't exist?
tmpdir=
version=0.10.10									# The version number of this script. If it ends in -wip it's work in progress, a
										#   version not meant for release. Other endings have more subtle meanings.
										#   Major versions (e.g. 1.x.x): Declared to be usable/somewhat good.
										#   Minor versions (e.g. x.1.x): Declared to have relevant changes/new features.
										#   Small versions (e.g. x.x.1): Declared to be publishable. Motivational release.
sitename="placeholder title"							# Fallback in case $sitename is not set through settings file or option.
url="https://www.example.com/"							# Fallback in case $url is not set through settings file or option.
style=elth				# Default: elth				# Set default style that is used if no style is set through settings file or option.
declare -i perpage=10 			# Default: 10				# Set default number of entries per page - If a list of entries (tagpage or all.html)
                								#   contains more than this amount of entries, it's split into several pages.

# default width/height for image gallery thumbnails in pixels - I say default, but there's currently no option to overwrite this (except with styles).
# shellcheck disable=SC2034
declare -i thumbsize=200		# Default: 200
# shellcheck disable=SC2034
declare -i minisize=60			# Default: 60
# shellcheck disable=SC2034
declare -i previewsize=800		# Default: 800
# shellcheck disable=SC2034
declare -i smallsize=600		# Default: 600

declare -A updateslist=()							# Will store names of entries for which a newer version exists. For cross-linking.
declare -A emails=()
maxfnl=255		# Default: 255, Minimum: 6, alt. format example: 8.3	# Maximum filename length in bytes without filename extension (e.g. '.html')
logfile="$(readlink -f SBWG.log)"
declare -i maxprocs=50			# Default: 50				# Set the maximum number of sub-processes allowed when parallelising functions.
declare -i max_entry_redirects=20	# Default: 20				# Maximum number of recursive redirections from 'redirect:' tags that are allowed.
declare -i max_debug_commands=10	# Default: 10				# Maximum number of recent commands that should be remembered in debug mode.

readonly scriptname="${0##*/}"
readonly warningsign='\xE2\x9A\xA0  '
readonly errorsign='\xE2\x9D\x8C '
readonly infosign='\xE2\x84\xB9  '
readonly debugsign='\xF0\x9F\x90\x9B '

waiting_ani_frames=( '▰▱▱▱▱▱▱▱▱▱' '▰▰▱▱▱▱▱▱▱▱' '▰▰▰▱▱▱▱▱▱▱' '▱▰▰▰▱▱▱▱▱▱' '▱▱▰▰▰▱▱▱▱▱' '▱▱▱▰▰▰▱▱▱▱' '▱▱▱▱▰▰▰▱▱▱' '▱▱▱▱▱▰▰▰▱▱' '▱▱▱▱▱▱▰▰▰▱' '▱▱▱▱▱▱▱▰▰▰'  '▱▱▱▱▱▱▱▱▰▰'  '▱▱▱▱▱▱▱▱▱▰'  '▱▱▱▱▱▱▱▱▱▱' )
declare -i waiting_ani_speed=10		# Default: 10				# Frame rate of the waiting animation in Hertz
declare -i waiting_ani_delay=3		# Default: 3				# Seconds that have to pass after last output before the waiting animation appears
declare -a parallel_pids=()							# Array stores PIDs of background processes belonging to the parallelisation feature




trap 'clean_up 1' SIGHUP SIGINT SIGTERM SIGQUIT					# Clean up if the script is killed.

# w():			Print a warning message - This is not for errors. Script will continue.
# Globals used:		$warningsign; $logfile
# Vars modified:	-
# Inputs:		$@: Warning message (singular)
# Outputs:		stderr: warning message; logfile: date, time and warning message
# Files written:	$logfile
# Returns:		-
w() {
  if option_set l; then								# If the logging option is set ...
    printf "${warningsign}" >> "${logfile}"					#   ... print warning icon and ...
    printf '%s: ' "$(date --utc)" >> "${logfile}"				#   ... then output a bug symbol ...
  fi
  printf "${warningsign}" 1>&2							# Print warning icon to stdout
  for str; do printf '%s' "${str}" 1>&2; done					#   ... and all of the message to stderr.
  printf '\n' 1>&2
  call_hook hook_warning "${@}"
#  >"${tmpdir}/warned"								# Remember that at least one warning has been given. Using a file for this so that
  printf '%s\n' "${BASH_LINENO[*]}" > "${tmpdir}/warned"								# Remember that at least one warning has been given. Using a file for this so that
  printf '%s\n' "${BASH_LINENO[*]}" > "$HOME/warned"								# Remember that at least one warning has been given. Using a file for this so that
										#   the information survives sub shells from variable assignments (grab_tags) and
										#   background processes (parallel mode).
										#   I could write the number of warnings into that file, but really, why?
}


# e():			Exit with error code after a cleanup attempt. Used to abort with a message in case of an error.
# Globals used:		$warningsign; $logfile
# Vars modified:	-
# Inputs:		$@: Error message (singular)
# Outputs:		stderr: error message; logfile: date, time and error message
# Files written:	$logfile
# Returns:		-
e() {
  if is_number "${1}" &>/dev/null; then
    local -i errcode="${1}"
    shift
  fi
  if option_set l; then								# If the logging option is set ...
    {
      printf "${errorsign}" >> "${logfile}"					#   ... print error icon and ...
      printf '%s: ' "$(date --utc)" >> "${logfile}"				#   ... then output a bug symbol ...
    } || w "Could not log debug message to '${logfile}'"
  fi
  printf "${errorsign}" 1>&2							# Print error icon to stdout
  for str; do printf '%s' "${str}" 1>&2; done					#   ... and all of the message to stderr.
  printf '\n' 1>&2
  call_hook hook_error "${@}"
  option_set F || clean_up "${errcode:-1}"					# Abort if force mode is not enabled.
  >"${tmpdir}/errored"								# Remember that at least one error has occured in case force more is enabled. Using
										#   a file for this so that the information survives sub shells from variable
										#   assignments (grab_tags) and background processes (parallel mode).
										#   I could write the number of errors into that file, but really, why?
}


# esub():		Provides a shorthand for existing the script cleanly and with an appropriate message in the case a subshell has existed with non-zero status.
#                         This is helpful if e() is used in a subshell (e.g. when assigned a variable) to make sure it actually exits the script/parent process.
# Globals used:		-
# Vars modified:	-
# Inputs:		-
# Outputs:		-
# Files written:	-
# Returns:		-
esub() {
  e "Error: A subshell has returned and brought me a bad exit status."
}


# clean_up():		Things that should be done if if the script crashes or is otherwise interupted.
# Globals used:		$tmpdir; $logfile; $shellbase; $date; $warningsign; $errorsign
# Vars modified:	-
# Inputs:		$1: Intended exit code
# Outputs:		stderr: messages; logfile: date, time and messages
# Files written:	"${shellbase}/last"
# Returns:		$1 (0 or error code), 2 if in doubt
clean_up() {
  if option_set_multi d; then
    printf 'The last %s commands were:\n' "${#command_history[@]}"
    local comm
    for comm in "${command_history[@]}"; do
      printf '%s\n' "${comm}"
    done
  fi
  tput cnorm									# Show the cursor again in case is was hidden at the time of clean-up.
  kill $(ps -o pid= --ppid $(cat -- \
    "${tmpdir}/ppid" &> /dev/null) &> /dev/null) &> /dev/null   		# Terminate all other processes that have been started, like background siblings
										#   and stuff. This is relevant for parallel mode.
#  [[ ${1} == 0 ]] || [[ -z ${1} ]] && {						# If this clean_up is called without an error/error code.
  [[ ${1} != 7 ]] && [[ ${1} != 5 ]] && {					# If this clean_up is called with an error code other than 7 or 5.
    if [[ -f ${odir}/lock ]]; then
      rm "${odir}/lock" || w "Output directory lock file removal issue."    	# Remove output directory lock file if it exists.
    fi
    if [[ -f ${shellbase}/lock ]]; then
      rm "${shellbase}/lock" || w "Input directory lock file removal issue."	# Remove input directory lock file if it exists.
    fi
  }

  # shellcheck disable=SC2016
#  [[ -f ${tmpdir}/warned ]] && {
#    w "There has been at least one warning. Check " \
#      "error output or log and see what you can do about it."
#    option_set l && w "View warnings from logfile with: " \
#      "grep ^'$(printf "${warningsign}")' '${logfile}'"
#  }
  if [[ -f ${tmpdir}/warned ]]; then
    w "There has been at least one warning. Check " \
      "error output or log and see what you can do about it."
    option_set l && w "View warnings from log file with: " \
      "grep ^'$(printf "${warningsign}")' '${logfile}'"
  fi
  # shellcheck disable=SC2016
  [[ -f ${tmpdir}/errored ]] && {
    w "There has been at least one error that has been ignored because " \
      "force mode was enabled. Check error output or log and see what you " \
      "can do about it."
    option_set l && w "View errors from log file with: " \
      "grep ^'$(printf "${errorsign}")' '${logfile}'"
  }
  [[ -z ${tmpdir} ]] || rm --recursive --force "${tmpdir}"			# Delete our temporary directory with all contents (if $tmpdir was even set).
  if [[ -n ${1} ]]; then
    d "Exiting with code ${1}."
    [[ -w ${shellbase}/last ]] && \
      printf '%s:%s' "${1}" "${date}" > "${shellbase}/last"			# Log complete or incomplete generation run with exit code and date.
    waiting_ani_stop								# Waiting animation has to be stopped after the last call of d().
    exit "${1}"									# Exit with provided error code.
  else
    d "Exiting without error code. (Was there maybe no error?)"
    [[ -w ${shellbase}/last ]] && \
      printf '%s' "${date}" >> "${shellbase}/last"				# Log date of last run without exid code.
    waiting_ani_stop								# Waiting animation has to be stopped after the last call of d().
    exit 255									# Exit with 255 if no exit code was provided.
  fi
  waiting_ani_stop								# This line should never be reached. Leaving this here just to be sure.
}

# v():			Function to output a string to stdout if verbose mode is enabled.
# Globals used:		$infosign; $logfile
# Vars modified:	-
# Inputs:		$@: Message (singular)
# Outputs:		stdout: message; logfile: date, time and message
# Files written:	$logfile
# Returns:		0 if output occurs, 1 if verbose mode disabled
v() {
  if option_set l; then								# If the logging option is set
    {
      printf "${infosign}" >> "${logfile}"					#   ... then output an info symbol ...
      printf '%s: ' "$(date --utc)" >> "${logfile}"				#   ... then output a bug symbol ...
      for str; do printf '%s' "${str}" >> "${logfile}"; done			#   ... and all of the message to the log file.
      printf '\n' >> "${logfile}"
    } || w "Could not log verbose message to '${logfile}'"
  fi
  if option_set v; then								# Only if in verbose or very verbose or debug mode ...
    waiting_ani_stop
    printf "${infosign}"							#   ... then display an info symbol ...
    for str; do printf '%s' "${str}"; done					#   ... and all of the message.
    printf '\n'
    waiting_ani_start
    return 0
  else
    return 1									# This enables executing a different command instead if verbose mode is not enabled.
  fi
}


# v():                  Function to output a string to stdout if very verbose mode is enabled.
# Globals used:         $infosign; $logfile
# Vars modified:        -
# Arguments:            $@: Message (singular)
# Outputs:              stdout: message; logfile: date, time and message
# Files written:        $logfile
# Returns:              0 if output occurs, 1 if very verbose mode disabled
vv() {
  if option_set l; then								# If the logging option is set
    {
      printf "${infosign}" >> "${logfile}"					#   ... then output an info symbol ...
      printf '%s: ' "$(date --utc)" >> "${logfile}"				#   ... then output a bug symbol ...
      for str; do printf '%s' "${str}" >> "${logfile}"; done			#   ... and all of the message to the log file.
      printf '\n' >> "${logfile}"
    } || w "Could not log very verbose message to '${logfile}'"
  fi
  if option_set_multi v; then							# Only if in very verbose or debug mode ...
    waiting_ani_stop
    printf "${infosign}"							#   ... then display an info symbol ...
    for str; do printf '%s' "${str}"; done					#   ... and all of the message.
    printf '\n'
    waiting_ani_start
    return 0
  else
    return 1									# This enables executing a different command instead if vv mode is not enabled.
  fi
}


debug_trap() {
  command_history+=("${BASH_COMMAND}")
  if (( ${#command_history[@]} > max_debug_commands )); then
    command_history=("${command_history[@]:1}")					# Remove oldest command.
  fi
}

# d():			Function to output a string to stdout if debig mode is enabled.
# Globals used:		$debugsign; $logfile
# Vars modified:	-
# Inputs:		$@: Message (singular)
# Outputs:		stdout: message; logfile: date, time and message
# Files written:	$logfile
# Returns:		-
# shellcheck disable=SC2032
d() {
  if option_set l; then								# If the logging option is set
    {
      printf "${debugsign}" >> "${logfile}"					#   ... then output a bug symbol ...
      printf '%s: ' "$(date --utc)" >> "${logfile}"				#   ... then output a bug symbol ...
      for str; do printf '%s' "${str}" >> "${logfile}"; done			#   ... and all of the message to the log file.
      printf '\n' >> "${logfile}"
    } || w "Could not log debug message to '${logfile}'"
  fi
  if option_set d; then								# Only if in debug mode ...
    waiting_ani_stop
    printf "${debugsign}"							#   ... then display this message with a bug symbol in front of it.
    for str; do printf '%s' "${str}"; done					#   ... and all of the message.
    printf '\n'
    waiting_ani_start
  fi
}


# o():			Output something to the current outfile (The outfile usually is an HTML file that is currently being generated.)
# Globals used:		$outfile
# Vars modified:	-
# Inputs:		$@: Command to execute
# Outputs:		stdout and stderr: Error message if command returned non-zero
# Files written:	$outfile
# Returns:		-
o() {
    "${@}" >> "${outfile}" || {
      d "The failed command looked kind of like this: ${*}"
      d "It was called in: ${FUNCNAME[*]}"
      e "Failed trying to execute a command that should write its output " \
      "to '${outfile}'. Please see error output for more information. Check " \
      "e.g. space, permissions and filename length. See '${scriptname} -h " \
      "options' on option '-R' for a solution for the latter or check your "\
      "tag values for forbidden characters."					# Output the rest of the arguments to the previously set $outfile.
    }
}


# c():			Output something to the current outfile and the current cachefile.
# Globals used:		$cachefile; $outfile
# Vars modified:	-
# Inputs:		$@: Command to execute
# Outputs:		stdout and stderr: Error message if command returned non-zero
# Files written:	$logfile; $outfile
# Returns:		-
c() {
    "${@}" | tee --append -- "${cachefile}" "${outfile}" > /dev/null || {
      d "The failed command looked kind of like this: ${*}"
      d "It was called in: ${FUNCNAME[*]}"
      e "Failed trying to execute a command that should write its output " \
      "to '${cachefile}' and '${outfile}'. Please see error output for more " \
      "information. Check e.g. space, permissions and filename length. See " \
      "'${scriptname} -h options' on option '-R' for a solution for the " \
      "latter or check your tag values for forbidden characters."		# Output the rest of the arguments to the previously set $outfile.
    }
}


#p():			Run a command; if parallel mode is enable: run the command in the background if not too many proccesses are running already and hold back
#			  its output until it's done. Don't forget to 'wait' after using p().
# Globals used:		$maxprocs; $options
# Vars modified:	-
# Inputs:		$@ Command to execute
# Outputs:		- (depends on the command though, obviously)
# Files written:	-
# Returns:		-
p() {
  if option_set Q; then								# If parallel mode is enabled
    while (( $(ps -eo ppid | grep -w $$ | wc -w) >= maxprocs )); do		# If too many subprocesses are already running
      if wait -n; then break; fi						# Wait for them except if wait fails or there is nothing to wait for.
    done
    "${@}" > >(sponge ) 2> >(sponge >&2) &
    parallel_pids+=($!)								# Add the PID of the process that has just bean created to the array so that the
  else										# If parallel mode is not enabled
    "${@}"									# Just run the command as it is
  fi
}


wait_for_p() {
  for pid in ${parallel_pids[@]}; do						# Wait until all background jobs belonging to the parallelisation feature ...
    wait ${pid}									#   ... have been finished or otherwise ended.
    parallel_pids=( "${parallel_pids[@]/${pid}}" )				# Remove the PID from the array of parallelised jobs still running.
  done
}

# predate():		Add the current date and time to each line of something.
# Globals used:		-
# Vars modified:	-
# Inputs:		-
# Outputs:		Current date and time, lines from stdin
# Files written:	-
# Returns:		-
predate() {
  while read -r line; do
    echo "$(date): ${line}"
  done
}


# trim():		Remove leading and trailing whitespaces from multiple lines.
# Globals used:		-
# Vars modified:	-
# Inputs:		stdin (multiple lines) or $@: String(s) from which whitespaces should be trimmed.
# Outputs:		Trimmed lines
# Files written:	-
# Returns:		-
trim() {
  if (( ${#} == 0 )); then
    local s
    while read -r s; do
      s="${s#"${s%%[![:space:]]*}"}"
      s="${s%"${s##*[![:space:]]}"}"
      printf '%s' "${s}"
    done
  else
    while (( ${#} > 0 )); do
      s="${s#"${s%%[![:space:]]*}"}"
      s="${s%"${s##*[![:space:]]}"}"
      printf '%s' "${1}"
      shift
    done
  fi
}


# decode_url():		Dedode a possibly partly percent-encoded URL.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: encoded URL string
# Outputs:		stdout: decoded URL string
# Files written:	-
# Returns:		-
# Borrowed (but changed) with gratitude from pure bash bible: https://github.com/dylanaraps/pure-bash-bible#decode-a-percent-encoded-string
decode_url() {
  : "${1//+/ }"
  printf '%b' "${_//%/\\x}"
}


# encode_url():		Percent-encode a URL.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: URL string
# Outputs:		stdout: encoded URL string
# Files written:	-
# Returns:		-
# Borrowed (but changed) with gratitude from pure bash bible: https://github.com/dylanaraps/pure-bash-bible#percent-encode-a-string
encode_url() {
  local LC_ALL=C
  local s="${1}"
  s="$(trim "${s}")"
  local -i i
  for (( i = 0; i < ${#s}; i++ )); do
    : "${s:i:1}"								# One character/byte per loop iteration
    case "$_" in
      [a-zA-Z0-9.~_+/:-])							# + and : stay because it's used a lot and looks better unencoded and shouldn't make
        printf '%s' "$_"							#   problems. / has to stay because encode_url() is used on entire paths and the
      ;;									#   slashes separating the path elements can't be encoded. Any slashes in any tag
      *)									#   values are removed/replaced early so that variables passed to encode_url contain
        printf '%%%02X' "'$_"							#   no slashes.
      ;;
    esac
  done
}


# encode_html():	Encode a string for literal use in a HTML document. Encode chacaters that are problematic in HTML or XML.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: String that may or may not contain any markup/HTML tags.
# Outputs:		String that won't contain any unencoded markup/HTML tags anymore.
# Files written:	-
# Returns:		-
encode_html() {
  printf '%s' "${1}" | sed 's/</\&lt;/g;s/>/\&gt;/g;s/\&/\&amp;/g;s/"/\&quot;/g;s/'"'"'/\&apos;/g;s/ /\&#32;/g'
}


# encode_xml():		Encode a string for literal use in an XML document. Encode chacaters that are problematic in or XML.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: String that may or may not contain any markup.
# Outputs:		String that won't contain any unencoded markup anymore.
# Files written:	-
# Returns:		-
encode_xml() {
  printf '%s' "${1}" | sed 's/</\&#38;#60;/g;s/>/\&#62;/g;s/\&/\&#38;#38;/g;s/'"'"'/\&#39;/g;s/"/\&#34;/g;s/ /\&#32;/g'
}


# encode_html_attr():	Encode string for use inside an HTML attribute. Replace or encode chacaters that are problematic in HTML attributes for various reasons.
# Globals used:		-
# Vars modified:	-
# Inputs:		$@ or stdin: String(s) that should be encoded for use inside HTML attribute value.
# Outputs:		Encoded string
# Files written:	-
# Returns:		-
encode_html_attr() {
  if (( ${#} == 0 )); then
    local s
    while read -r s; do
      printf '%s' "${s//#/_}" | tr "\n" " " | sed 's/\&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g;s/"/\&quot;/g;s/'"'"'/\&#39;/g;s/[.]/_/g'
    done
  else
    while (( ${#} > 0 )); do
      printf '%s' "${1//#/_}" | tr "\n" " " | sed 's/\&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g;s/"/\&quot;/g;s/'"'"'/\&#39;/g;s/[.]/_/g'
      shift
    done
  fi
}


# encode_xml_attr():	Encode string for use inside an XML attribute. Replace or encode chacaters that are problematic in XML attributes for various reasons.
# Globals used:		-
# Vars modified:	-
# Inputs:		$@ or stdin: String(s) that should be encoded for use inside XML attribute value.
# Outputs:		Encoded string
# Files written:	-
# Returns:		-
encode_xml_attr() {
  if (( ${#} == 0 )); then
    local s
    while read -r s; do
      printf '%s' "${s}" | tr "\n" " " | sed 's/\&/\&#38;#38;/g;s/</\&#38;#60;/g;s/>/\&#62;/g;s/'"'"'/\&#39;/g;s/"/\&#34;/g'
    done
  else
    while (( ${#} > 0 )); do
      printf '%s' "${1}" | tr "\n" " " | sed 's/\&/\&#38;#38;/g;s/</\&#38;#60;/g;s/>/\&#62;/g;s/'"'"'/\&#39;/g;s/"/\&#34;/g'
      shift
    done
  fi
}


# pathreducer():	Remove forbidden characters and shorten a path if necessary. Convert long path string into a path string whose elements each are at most
#			  $maxfnl bytes long while keeping them sort of unique.
# Globals used:		$maxfnl
# Vars modified:	-
# Inputs:		$1: Original (possibly long) path (rel or ab). Last element should be file name, not directory name ($1 must not end with a directory name).
# Outputs:		String of reduced path
# Files written:	-
# Returns:		-
pathreducer() {
  call_hook hook_pathreducer
  if option_set R; then								# If the option -R/--reduce-paths is set.
    (										# Put this in a subshell so that the LANG and LC_ALL settings don't last.
      LANG=C LC_ALL=C
      IFS='/' read -ra path <<< "${1}"
      maxbl=${maxfnl%.*}							# Extract/set the max. basename length. Will be same as $maxfnl if there is no period
      partno=1
      for part in "${path[@]}"; do
        call_hook hook_pathreducer_part_start
        newsuff=''
        if (( partno == ${#path[@]} )); then					# If this is the last element in the path (meaning the file name, not a directory
                                                                                #   name) Filename extensions should only be handled for the last path element,
                                                                                #   which will presumably be a file name and not a directory name. Some filesystems
                                                                                #   can't have dots in directory names.
          suff="${part##*.}"
          if [[ ${part} == *.* ]] && [[ -n ${suff} ]] \
            && ! [[ ${suff} =~ [^[:alnum:]] ]] && (( ${#suff} <= 12 )); then	# If the element contains a period and there is anything after its last period ...
          									#    ... and that anything is only numbers and letters and no longer than 12 bytes.
										#    ... then this suffix will be assumed to resemble the filename extension.
            # Reduced suffix cut off after $maxsl bytes. (+1 because the period is included)
            newsuff=".$(printf '%s' "${suff}" | tr --delete --complement '[:alnum:].' | tr '[:upper:]' '[:lower:]')"
            if [[ ${maxfnl} == *?.?* ]]; then					# If there is a period in the definition of the maximum filename length.
              maxsl=${maxfnl#*.}						# Extract/set the maximum suffix length.
              # Cut off the suffix after $maxsl bytes.
              newsuff="$(printf '%s' "${newsuff}" | head --bytes $((maxsl+1)))"
              additional_cut_off_from_base=0
            else								# If no definition for the maximum suffix length is set
              additional_cut_off_from_base=${#newsuff}				# Remember the number of characters that have to be cut off the basename in order
            									#   to not exceed the maximum filename length when the suffix is included.
              if (( $((maxfnl - additional_cut_off_from_base)) < 6 )); then	# If the reduced filename would became shorter than 6 bytes (which is the minimum
            									#   that is required to add the hash)
                newsuff="$(printf '%s' "${newsuff}" | head --bytes $((maxfnl-6)))"	# Shorten the suffix to be short enough to be included.
                # shellcheck disable=SC2016
                w "Pathreducer tried to shorten the path element '${part}' " \
                  "to be shorter than 0 bytes. You know that's not possible. " \
                  "Instead it cut off the filename extension" \
                  "${newsuff:+ to '$newsuff'}."					# Warn that the suffix was cut off despite there being no maximum suffix length
                additional_cut_off_from_base=${#newsuff}
              fi
              if [[ ${newsuff} == . ]]; then
                newsuff=''							# Result should never end in a dot. (e.g. when the suff had to be cut off)
                additional_cut_off_from_base=0
              fi
            fi
          else                                                                  # If the file name doesn't appear to have what I decided to count as a filename
                                                                                #   extention (a dot in the filename followed by no more than 12 alnum characters).
            additional_cut_off_from_base=0
          fi
        else                                                                    # If this is not the last element in the path.
          additional_cut_off_from_base=0
        fi
        if [[ -n ${newsuff} ]]; then
           base="${part%.*}"							# This is the basename if there is a suffix, the whole part if there is no suffix.
        else
          base="${part}"
        fi
        # Reduced basename.
        newbase="$(printf '%s' "${base}" | tr -dc '[:alnum:]._-' | tr '[:upper:]' '[:lower:]')"

        if [[ ${maxfnl} == *?.?* ]]; then					# If there is a period in the definition of the maximum filename length.
          newbase="$(printf '%s' "${newbase}" | tr --delete --complement '[:alnum:]._-')"		# Make sure there are no dots in the basename fot SFN FAT systems.
        fi
        if [[ ${newbase: -1} == . ]]; then newbase="${newbase::-1}"; fi		# Result should never end in a dot.
        call_hook hook_pathreducer_part_before
        if (( $((${#base}+${#newsuff})) > ${maxbl:-255} )) \
          || [[ ${newbase} != "${base}" ]]; then				# If the basename is longer than the maximum basename length or would change
        									#   when special characters are removed and all letters are made lower case.
          call_hook hook_pathreducer_part_between
          printf '%s' "${newbase}" \
            | head --bytes $((${maxbl:-255}-6-additional_cut_off_from_base))	# Use reduced version of this element of the path and print as much of the string
          									#   as will fit, no newline and ...
          printf '%s' "$(sha1sum -- \
            <(printf '%s' "${base}${newsuff}")|head --bytes 6)${newsuff}"	#   ... add the hash, resulting in a string exactly $maxfnl bytes long.
          									#   The extra printf encapsulation is to prevent a newline at the end.
        else									# If the basename is not too long and does not contain any special characters or
        									#   ... uppercase letters
          printf '%s' "${base}${newsuff}"					#   ... print it as it is.
        fi

        call_hook hook_pathreducer_part_after
        [[ ${partno} != "${#path[@]}" ]] && printf '/'				# Add slash if this is not the last element of the path.
        partno=$(( partno + 1 ))
      done
    )
  else										# If option -R/--reduce-paths is not enabled
    printf '%s' "${1}"								# Just echo the string as it is.
  fi
}


# call_hook():		If the hook is declared anywhere (commonly in the web site's settings file) then it's called with all arguments starting with the second one.
# Globals used:		$outfile
# Vars modified:	-
# Inputs:		$1: Name of the function to be called; $2...: Arguments passed to hook
# Outputs:		stdout: debug message
# Files written:	-
# Returns:		0 if hook is declared, 1 if not
call_hook() {
  local f="${1}"
  if declare -F "${f}" > /dev/null; then					# If $f is a function.
    # shellcheck disable=SC2016
    d "Executing hook '${f}'.${outfile+ - Current output file is '$outfile'.}"	# Notify that a hook will be executed, print the $outfile if one is set.
    shift									# Remove the function name from the list of arguments ...
    ${f} "${@}"									# ... and call the function with the remaining arguments.
    return 0									# Return 0 after the function was called if the function exists.
  else
    return 1									# Return 1 of no function of that name is known.
  fi
}

# waiting_ani_start():	Starts the wating animation in the background, remembering its PID.
# Globals used:		$waiting_ani_pid
# Vars modified:	-
# Inputs:		-
# Outputs:		-
# Files written:	-
# Returns:		-
waiting_ani_start() {
  if option_set w && ! option_set Q && ! ps -${waiting_ani_pid} &> /dev/null	# If a waiting animation called with this function isn't already running or parallel
#  if option_set w && ! option_set Q && ! timeout --preserve-status \
#    --kill-after=5s 1s ps -${waiting_ani_pid} &> /dev/null			# If a waiting animation called with this function isn't already running or parallel
  then										#   mode is enabled and if the waiting animation option is set in the first place.
#										#   Using timeout as an attempted workaround around a bug that sometimes hangs SBWG.
    tput civis									# Hide the cursor.
    show_waiting_ani&								# Start the waiting animation in background (see conditions in show_waiting_ani()).
    waiting_ani_pid=$!								# Remember the PID of the waiting ani background process so it can be killed later.
  fi
}

# waiting_ani_stop():	Stops the wating animation that is running in the background if its PID was remembered.
# Globals used:		$waiting_ani_pid
# Vars modified:	-
# Inputs:		-
# Outputs:		-
# Files written:	-
# Returns:		-
waiting_ani_stop() {
  { kill ${waiting_ani_pid} && wait ${waiting_ani_pid}; } 2>/dev/null 		# Kill the background process that displays the waiting animation
  tput cnorm									# Show the cursor again.
}

show_waiting_ani() {
  sleep "${waiting_ani_delay}"							# Wait before starting the animation so that it looks cleaner. Only if there is no
										#   output for a while the animation makes any sense.
  local cursor_position
  exec < /dev/tty								# These few lines are for reading silently the current cursor position in the ...
  local oldstty									#   ... terminal. Just printing and reading the escape sequence would leave the ...
  oldstty=$(stty -g)								#   ... column and row printed on the terminal and the more often the current ...
  stty raw -echo min 0								#   ... cursor position would be read the more unwanted numbers would be left on ...
  printf '\033[6n' > /dev/tty							#   ... the command prompt after the script finished. This is why the tty is ...
  IFS=';' read -r -d R -a cursor_position					#   ... changed for the printing and reading back the current cursor position ...
  stty $oldstty									#   ... and then changed back to the old stty so wanted output can be printed again.
  (( ${cursor_position[1]} > 1 )) &>/dev/null && printf '\n'			# Go to new line if the cursor is currently not at the very left.
										#   The idea here is that if anything unexpectedly prints to the terminal (anything
										#   other than one of the functions v/vv/d, like an error or warning from the script
										#   or some external programme printing to sterr or stdout, that output will
										#   probably be long enough to overwrite the last animation frame. If that happened,
										#   start in a new line instead of printing the next animation frame in the same line
										#   the error message was printed to.
  while true; do								# Do the animation until the process is killed.
    for waiting_ani_frame in ${waiting_ani_frames[@]}; do
      printf '\033[2K\r'							# Start at the beginning of an empty line. (Overwriting unwanted characters from the
										#   curser position detection.)
      printf '%s\r' "${waiting_ani_frame}"					# Print the actual animation frame and immedietely return to begginning ofline.
      sleep $(printf '%s\n' "scale=3; 1/${waiting_ani_speed}" | bc)		# Turn the speed value from hertz into seconds and wait for that long.
    done
  done
}


# is_number():		Check if the argument is a simple number.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: Number
# Outputs:		stderr: warning message
# Files written:	-
# Returns:		0 if $1 is a simple number, 1 if not, 2 if it's empty
is_number() {
  if (( ${#1} > 19 )); then w "'${1}' is too long to be a number that is not overflowing in bash."; fi
  case ${1} in
    '') w "Supplied argument is empty. So, no, not a number."; return 2 ;;
    *[!0123456789]*) d "'${1}' has a non-digit somewhere in it."; return 1 ;;
    *) d "'${1}' is strictly numeric and positive."; return 0 ;;
  esac >&2
}


# tag_exists():		Check whether a string is an item in the tagslist (whether a specific tag exists in any entry of the web site)
# Globals used:		$tagslist
# Vars modified:	-
# Inputs:		$1: Tag
# Outputs:		-
# Files written:	-
# Returns:		0 if tag exists, 1 if not
tag_exists() {
  local tag
  for tag in "${tagslist[@]}"; do
    [[ "${tag}" == "${1}" ]] && return 0
  done
  return 1
}


# has_flag():		Check whether a string is contained in the flags variable (that var should be filled with the value of the flags tag of the currently
#                         processed entry).
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: flag
# Outputs:		-
# Files written:	-
# Returns:		0 if tag exists, 1 if not
has_flag() {
  [[ ${flags} == *${1}* ]]
}


# grab_tag():		Extract a single tag's value from a list of tag lines.
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: Newline separated list of tags; $2: tag type; $3: default value (optional)
# Outputs:		Value of the first tag of the type $2 in the list $1; $3 if no tag of that type is in the list
# Files written:	-
# Returns:		1 if not enough arguments, 0 otherwise
grab_tag() {
  if (( ${#} < 2 )); then return 1; fi						# If not at least 2 arguments are given, fail.
  local ret
  ret="$(printf '%s' "${1}" \
    | grep --text --max-count 1 -- "^${2}:" \
    | cut --characters $(( ${#2}+2 ))-)"					# Find tag if it exists, first match, cut off its name.
  ret="${ret:-$3}"								# If empty (no tag occurance found) use default/fallback value.
  call_hook kook_grab_tag
  printf '%s' "${ret}"
  return 0
}


# looks_like_tag():	Determin how likely it is that the passed string is a tag line from an entry or page source header/how much it looks like a tagline.
#			  Used in grab_tags().
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: String to check whether it looks like a tag line
# Outputs:		stderr: warning message
# Files written:	-
# Returns:		0 if $1 looks like a tagline, 1 if not quite, 2 if definitely not
looks_like_tag() {
  local -i num=100
  case "${1:0:10}" in								# The first 10 characters of the line
    *([[:space:]])) num=1 ;;							# Contains only whitespaces/is practically empty
    author:*|cat:*|top:*|lang:*|created:*|edited:*|sort:*|title:*|note:*|\
    redirect:*|re:*|reply:*|ref:*|updateof:*|rev:*|flags:*|\#*) num=0 ;;	# Start with one of these tag types
    +([[:alnum:]]):[^[:space:]]*) num=1
      w "Ignored tagline in '${f}' (unfamiliar tag): '${1}'" ;;			# The problem with this is, although it does print the warning, it's not remembered
										#   because grab_tags(), from where looks_like_tag() is called, is usually called
										#   inside a subshell. I don't know how to fix this without a stupidly big hassle
										#   or using exit codes through multiple levels every time I want to assign the
										#   output of grab_tags() to a variable.
    "<!--"*) num=1 ;;								# Start with an HTML comment opening (for comapibility with earlier versions)
    "-->") num=1 ;;								# Are only an HTML comment closing (for comapibility with earlier versions)
    *) num=2 ;;
  esac
  return ${num}
}


# filter_tag():		Remove broken unicode character from a string (uses perl because I don't know how it can be done nicely in Bash)
# Globals used:		-
# Vars modified:	-
# Inputs:		stdin: string that may have to be filtered
# Outputs:		stdout: filtered string
# Files written:	-
# Returns:		-
filter_tag() {
perl /dev/fd/3 3<<'EOPERL'
use strict;
use warnings;
use Encode;
my $buffer = '';
while (1) {
  if (4 >= length $buffer and defined fileno STDIN) {
    read STDIN, $buffer, 65536, length $buffer
      or close STDIN;
  } elsif (length $buffer) {
    substr $buffer, 0, 1, '';
  } else {
    last;
  }
  $_ = Encode::decode 'UTF-8', $buffer, Encode::FB_QUIET;
  s/(?![\t\n])\pC//g;
  print Encode::encode 'UTF-8', $_;
}
EOPERL
}


# grab_tags():		Extract the tags (source file header) from a SBWG content file filtering each tagline.
# Globals used:		$1 (nameref); $cachedir; $shellbase
# Vars modified:	-
# Inputs:		$1: Filename/path from which the tag header should be extracted
# Outputs:		List of filtered tags
# Files written:	-
# Returns:		-
grab_tags() {
  (( ${#} == 1 )) || e "Error: I am confused inside. Looks like you found a bug."
  [[ ${1} == '' ]] && e "Error: I am left bewildered. Looks like you found a bug."
  [[ -d ${1} ]] && e "Tried to use directory as a cache file: '${1}'"
  local cachefile
										#   ... go by the path of the source file that's already unique.
  set_cachefile tags "$(pathreducer "${1#$shellbase/}")"
  if [[ -r ${cachefile} ]]; then						# If there already is a cache file for this entry/page and the file can be read ...
    cat "${cachefile}" || w "Could not use tags cache file '${cachefile}'."	#   ... just use the contents of the existing cache file and warn if not possible.
  else										# If there is no readable cache file for this entry/page existing, generate it.
    local f="${1}"
    local -i linenum=1
    local -i nontaglines=0
    local t
    while IFS= read -r line; do
      looks_like_tag "${line}"
      n=${?}
      if (( n == 0 )); then							# If the line looks like a tagline
        if command -v perl > /dev/null; then					# If perl is available
          line="$(printf '%s' "${line}" | filter_tag)"				# Filter out control characters and broken multibyte unicode characters.
        fi
        line="$(printf '%s\n' "${line}" | trim  | sed 's/:$//' )"		# Trim whitespaces and cut the last character from the line if it's a colon.
        local tmpline
        tmpline="$(printf '%s\n' "${line#*:}" | trim )"				# Trim whitespaces after the tag type before the tag value and again at the end.
        line="$(printf '%s' "${line%%:*}:${tmpline}")"				# Put the tag type and tag value back together.
        case "${line}" in
          cat:*|top:*|author:*|lang:*)
            line="${line//\//-}"
          ;;&									# Slashes can't be part of a filename.
          title:*|created:*|edited:*|sort:*|re:*|ref:*|reply:*|redirect:*|\
          updateof:*|rev:*)
            if [[ ${t} == *$'\n'${line%:*}* ]]; then				# If the tags gathered so far already contain a line starting with the same tagtype
              line="(${line})"							# Add the redundant tag to the tags variable in an altered (surrounded by brackets)
										#   way so it is neither lost nor used.
            fi
          ;;&
          *)
            line="${line//$'\t'/ }"						# Why is this even in the case statement if it gets applied in any case? Anyway.
          ;;
        esac
        t="${t}"$'\n'"${line}"							# ... add it to the block that will be echoed to where the function was called from
        nontaglines=0								# Set counter back to 0. Only care about successive lines that don't look like tags.
      else									# If the line does not look like a tagline
        nontaglines=$(( nontaglines + n))					# Keep track of how many lines in a row didn't look like a tagline.
      fi
      (( nontaglines >= 3 )) && break						# If it's three in a row already, don't bother to check the rest/following lines.
      linenum=$(( linenum + 1))							# If the end of the loop is reached, increase line counter for the next iteration.
    done <<< "$(cat "${f}")"$'\n'$'\n' || e "Could not grab tags from '${f}'."	# Adding two linefeeds so that lines in entry source files with less than two lines
										#   of non-tag content are counted correctly.
    linenum=$(( linenum - 2))							# Have to substract the line that has been counted despite not looking like a tag.
    t="taglines:${linenum}${t}"							# Use the always empty first line to store the line counter so that we know later
    t="$(printf '%s\n' "${t}"|sort --field-separator=: -k1,1 -k2,2|uniq)"	# ...
										#   Sort tags according to how I happened to like them today and remove duplicates.
										#   This way the author tag should always end up first, which is useful for
										#   substituting it with an avatar picture or using CSS for something similar.
    ref="${t}"
    :|install -D /dev/stdin "${cachefile}" \
      || w "Cache file '${cachefile}' can not be created."			# Make sure the cache file and all directories above it exist.
    call_hook hook_grab_tags
    printf '%s' "${t}" | tee "${cachefile}"					# Echo the result, one tag per line to be parsed by other funtions ...
										#   ... and save it to the cache file so that the tags don't need to be checked ...
										#   ... all again the next time the function is called.
  fi
  return 0									# Don't return non-zero except if explicit err occurred.  May be relevant for esub().
}


# add_navbar():		Appends the navigation bar, as previously generated by gen_navbar(), to the file whose path and name is currently stored in $outfile.
# Globals used:		$cachefile; $outfile
# Vars modified:	-
# Inputs:		-
# Outputs:		stderr: error message
# Files written:	$outfile
# Returns:		-
add_navbar() {
  o printf '<a id="menu-link" href="'						# Add a link to navbar.html that can be used e.g. by minimalistic mobile themes.
  o encode_url "$(pathreducer /navbar.html)"
  o printf '">Menu</a>'
  o printf '<section>'								# This is just to enable styling.
  local cachefile
  set_cachefile navbar
  # shellcheck disable=SC2015
  [[ -f ${cachefile} ]] \
    && o cat "${cachefile}" \
    || e "The navbar file is not accessible or could not be added."		# Include the actual side bar/navbar from the temp dir, if the file exists.
}

# add_content():	Prints the content part of a source file to the current outfile. Expects list of taglines of the source file in question to be in $tags.
# Globals used:		$tags; $outfile
# Vars modified:	-
# Inputs:		$1: Path to source file
# Outputs:		stderr: error message
# Files written:	$outfile
# Returns:		-
add_content() {
  # shellcheck disable=SC2015
  [[ -f ${1} ]] \
    && o awk "NR > $(grab_tag "${tags}" taglines) { print }" \
    < "${1}" \
    || e "Content from source file '${1}' can't be used. " \
    "Is the file accessible?"	                                        	# Print the content of the entry file (without the tag header) to the outfile.
}

# add_footer():		Appends the footer file to the outfile.
# Globals used:		$shellbase
# Vars modified:	-
# Inputs:		-
# Outputs:		stderr: warning message
# Files written:	$outfile
# Returns:		-
add_footer() {
  # shellcheck disable=SC2015
  [[ -f ${shellbase}/footer ]] && o cat "${shellbase}/footer"
  call_hook hook_footer
  o printf '  </body>\n</html>'
}



# redirect():		Creates hard link if possible, otherwise creates HTML file that just redirects to a URL.
# Globals used:		$odir
# Vars modified:	-
# Inputs:		$1: Path of the link or HTML file that should redirect; $2: Path of target file (where to redirect to)
# Outputs:		-
# Files written:	$odir/$1
# Returns:		-
redirect() {
  call_hook hook_redirect "${@}"
  if ! [[ -f ${odir}/${2} ]]; then						# If the target file doesn't exist already, create an empty one for now. ...
    :> "${odir}/${2}" || return 1						#   ... This may happen e.g. in some cases of recursive entry redirections.
  fi
  if [[ -f ${odir}/${1} ]]; then						# If there already is a file present as a source (it might already be the right
										#   file created by ln in a previous run of SBWG.)
    rm "${odir}/${1}" || return 2						#   then it has to be removed (otherwise an endless loop may be created if ln fails
										#   because the file already exists and SBWG writes an HTML redirect to itself.
										#   If removing it fails it's the same: No HTML redirect should be created.
  fi
  ln -- "${odir}/${2}" "${odir}/${1}" ||					# Create hard link or, if that fails ....
    printf \
      '<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="refresh" content="0; url=/%s" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <a href="/%s" title="Follow this link to continue.">Follow this link to continue.</a>
  </body>
</html>' \
      "$(encode_url "${2}")" "$(encode_url "${2}")" \
        > "$(encode_url "${odir}/${1}")" &&					#   ... create simple HTML document with a link + automatic redirect to the target.
      return 0									# Returns 0 if either link or HTML redirection has been created.
}


# prepare():		Preparations that will be performed regardless of the given command line options.
# Globals used:		$version; $tmpdir; $shellbase; $odir; $maxfnl; $desired_entry_name; $desired_entry_file; $desired_page_name; $desired_page_file;
#			  $desired_gallery; $desired_tagpage; $desired_author
# Vars modified:	$tmpdir; $cachedir; $shellbase; $odir; $maxfnl; $desired_entry_name; $desired_entry_full; $desired_entry_file; $desired_page_name;
#			  $desired_page_full; $desired_page_file; $desired_tagpage; $desired_author
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	-
# Returns:		-
prepare() {
  d "SBWG version: ${version}"
  d "tmpdir: ${tmpdir}"
  printf $$ > "${tmpdir}/ppid"							# Remember the PID of the process that did prepare() because assumably it is the
										#   parent of any subprocesses of this script, eg. function run in the background.
  if option_set C; then
    [[ -z ${desired_caches} ]] || [[ ${desired_caches} == all ]] \
      && desired_caches=navbar,tags,entries,blog,pages,galleries,all		# If persistant caches are enabled but no individual parts have been specified,
										#   specify all possible parts.
    d "Cachegroups: '${desired_caches}'"
  fi
  if [[ ${shellbase: -1} == / ]]; then shellbase="${shellbase::-1}"; fi		# Remove trailing slash if there was one so we can be sure what we have.
  d "shellbase: ${shellbase}"
  if [[ ${odir: -1} == / ]]; then odir="${odir::-1}"; fi			# Remove trailing slash if there was one so we can be sure what we have.
  d "odir: ${odir}"
  vv "Input directory '${shellbase}'"
  vv "Output directory '${odir}'"
  if ls "${odir}"/* &> /dev/null; then
    vv "The output directory is not empty. " \
      "If you want to start clean, empty or remove the directory manually."
  fi

  mkdir --parents -- "${odir}" || e 5 "Output directory not available."
  [[ -w ${odir} ]] || e 5 "Output directory is not writable."

  if [[ -f ${shellbase}/lock ]]; then						# If the input directory is locked
    e 7 "The input directory is locked. Remove '${shellbase}/lock' if you " \
      "are sure, and continue/start SBWG again."				# error and exit if input dir lock file exists
  else
    :> "${shellbase}/lock" && [[ -f ${shellbase}/lock ]] \
      || e 5 "Lock file could not be created at '${shellbase}/lock'."		# create input dir lock file if it doesn't exist, error and exit if it can't be
  fi										#   created or isn't there after it should have been created
  if [[ -f ${odir}/lock ]]; then						# If the output directory is locked.
    e 7 "The output directory is locked. Remove '${odir}/lock' if you are " \
      "sure, and continue/start SBWG again."					# error and exit if output dir lock file exists
  else
    :> "${odir}/lock" && [[ -f ${odir}/lock ]] \
      || e 5 "Lock file could not be created at '${odir}/lock'."		# create output dir lock file if it doesn't exist, error and exit if it can't be
  fi										#   created or isn't there after it should have been created

  if option_set R; then
    d "given maxfnl: $maxfnl"
    local maxbl="${maxfnl%.*}"							# Extract/set the max. basename length. Will be same as $maxfnl if there is no period
    if ! is_number "${maxbl}"; then
      e "The maximum filename length without filename extension is " \
        "defined as '${maxbl}' bytes. '${maxbl}' is not a number, dude."
      maxbl=255
      w "Defaulting maximum filename length without filename extension to " \
        "${maxbl} bytes."
    fi
    if (( maxbl < 6 )); then
      e "The maximum filename length without filename extensions is " \
        "defined as ${maxbl} bytes. It can not be smaller than 6 bytes " \
        "though."
      maxbl=6
      w "Defaulting maximum filename length without filename extensions to " \
        "${maxbl} bytes."
    fi
    if [[ ${maxfnl} == *.* ]]; then 						# If there is a period in the definition of the maximum filename length.
      local maxsl="${maxfnl#*.}"						# Extract/set the maximum suffix length.
      if ! is_number "${maxsl}"; then
        e "The maximum filename extensions length is defined as '${maxsl}' " \
          "bytes. '${maxsl}' is not a number, dude."
        maxfnl="${maxbl}"
        w "Filename suffixes will not be shortened."
      else									# If there is a definition for the suffix length and it is a number.
        maxfnl="${maxbl}.${maxsl}"
      fi
    else									# If it's not a two-number value
      maxfnl="${maxbl}"								# Assign the maximum basename length that already has been checked above.
    fi
    d "using maxfnl: $maxfnl"
  fi

    if [[ -n ${desired_entry_name} ]]; then					# If an entry name was given with option -e to generate a single entry.
      desired_entry_name="$(printf %s "${desired_entry_name}" \
        | tr '/' '-' | tr '\t' ' ')"						# Apply the same filter that is applied in grab_tags().
      if desired_entry_full="$(find "${shellbase}/entries/" \
        -name "${desired_entry_name}" -type f 2> /dev/null | grep .)"; then	# See whether a file of this name is in or somewhere below the entries directory.
        vv "Will generate the single entry named '${desired_entry_name}'."
      else
        e "The desired entry '${desired_entry_name}' " \
          "does not exist in the entries directory."				# Error if the entry does not exist.
      fi
    fi
    if [[ -n ${desired_entry_file} ]]; then					# If an entry filename was given with option -E to generate a single entry.
      desired_entry_file="$(printf %s "${desired_entry_file}" \
        | tr '\t' ' ')"                                                         # Apply the same filter that is applied in grab_tags().
      desired_entry_file="$(readlink -f "${desired_entry_file}")"		# Turn the possibly relative path into an absolute path.
      if [[ -f ${desired_entry_file} ]]; then					# If there actually is a file there
        if [[ $(file --mime-type -- ${desired_entry_file}) == *text/* ]]; then	# If the desired file looks like a text file
          vv "Will generate the single entry from '${desired_entry_file}'."
        else									# todo: Meh, I've never seen that happen, even with non-text files.
          e "The desired entry '${desired_entry_file}' is not a text file."	# Error if it's not a text file.
        fi
      else
        e "The desired entry '${desired_entry_file}' does not exist."		# Error if the file does not exist.
      fi
    fi
    if [[ -n ${desired_page_name} ]]; then					# If a page name was given with option -p to generate a single page.
      desired_page_name="$(printf %s "${desired_page_name}" \
        | tr '/' '-' | tr '\t' ' ')"
      if desired_page_full="$(find "${shellbase}/pages/" \
        -name "${desired_page_name}" -type f 2> /dev/null | grep .)"; then	# See whether a file of this name is in or somewhere below the pages directory.
        vv "Will generate the single page named '${desired_page_name}'."
      else
        e "The desired page '${desired_page_name}' does not exist " \
          "in the pages directory."						# Error if the page does not exist.
      fi
    fi
    if [[ -n ${desired_page_file} ]]; then					# If a page filename was given with option -P to generate a single page.
      desired_page_file="$(printf %s "${desired_page_file}" \
        | tr '\t' ' ')"
      desired_page_file="$(readlink -f "${desired_page_file}")"		        # Turn the possibly relative path into an absolute path.
      if [[ -f ${desired_page_file} ]]; then					# Only absolute paths are possible that way. I'll accept that for now.
        if [[ $(file --mime-type -- ${desired_page_file}) == *text/* ]]; then	# If the desired file looks like a text file
          vv "Will generate the single page from '${desired_page_file}'."
        else
          e "The desired entry '${desired_page_file}' is not a text file."	# Error if it's not a text file.
        fi
      else
        e "The desired page file '${desired_page_file}' " \
          "does not exist."							# Error if the file does not exist.

      fi
    fi

  if option_set g || option_set G; then
    command -v convert &>/dev/null || e "I need ImageMagick or some " \
      "compatible version of convert in order to create the different " \
      "gallery image sizes. If there is one, I didn't see it, sorry. Maybe " \
      "create an alias for 'convert' to point me to it. You can use 'alias "\
      "convert=:' before running the script or added to the settings file " \
      "to disable the usage of convert."
    if [[ -n ${desired_gallery} ]]; then					# If a gallery name was given with option -g to generate a single gallery.
      if [[ -d ${shellbase}/galleries/${desired_gallery} ]]; then		# See whether a directory of this name is in the galleries directory.
        vv "Will generate the single gallery named '${desired_gallery}'."
      else
        e "The desired gallery '${desired_gallery}' does not exist " \
          "in the galleries directory."						# Error if the gallery does not exist.
      fi
    fi
  fi

  [[ ${desired_tagpage} ]] \
    && desired_tagpage="$(printf '%s\n' "${desired_tagpage}" \
      | trim | sed 's/\//-/g;s/\t/ /g;s/:$//')" \
    && vv "Will generate the single tagpage '${desired_tagpage}'."		# Apply the same filters that are applied in grab_tags().

  [[ ${desired_author} ]] && {
    desired_author="$(printf '%s\n' "${desired_author}" \
      | trim | sed 's/\//-/g;s/\t/ /g;s/:$//')"					# Apply the same filters that are applied in grab_tags().

    v "Ignoring entries that are not tagged 'author:${desired_author}'."	# Inform if only content from only one author will be generated.
  }
  if option_set n; then gen_navbar; fi						# Generate navbar if option n is set. Option n also gets set along with options
										#   c, b, p, P, e, E, t, g, G and r.
  call_hook hook_prepare
  v "Finished basic preparations. ✅"
}


# copy_styles():	Copy CSS files belonging to the chosen style set into the css directory of the outout directory.
# Globals used:		$style; $shellbase; $odir
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	"$odir/styles/"*
# Returns:		-
# Called if option -s or -c is set. The assumption that every complete website contains at least one CSS file is almost true after all.
# The global variable $style has to be set. If it's not set through an option or setting in the settings file, the default that is set at the top of this script ...
# ... is used.
copy_styles() {									# $1: Style prefix / style set name
  [[ -z ${style} ]] && return							# If the style variable is set empty or unset, no style shall be used.
  vv "Copying styles..."
  mkdir --parents -- "${odir}/styles" || e "css directory is not available."
  d "style: ${style}"
  if option_set R; then								# If pathreducer mode in enabled
#    for file in "${shellbase}/styles/${style}"{,.*,-*.*}; do
    for file in "${shellbase}/styles/${style}"{,.*,-*}; do
      cp --update --no-preserve=all -- "${file}" \
        "${odir}/$(pathreducer "styles/${file##*/}")" &>/dev/null		# Copy each file that belongs to the chosen styleset individually, reducing the
										#   filename if necessary. Get rid of the output. Who cares that some of those files
										#   may not exist
    done
  else
#    cp --update --no-preserve=all -- \
#      "${shellbase}/styles/${style}"{,.*,-*.*} \
#      "${odir}/styles/" &>/dev/null						# Copy all files that belong to the chosen style set to the styles directory in the
    cp --update --no-preserve=all -- \
      "${shellbase}/styles/${style}"{,.*,-*} \
      "${odir}/styles/" &>/dev/null						# Copy all files that belong to the chosen style set to the styles directory in the
										#   output directory. Get rid of the output. Who cares that some of those files may
										#   not exist
  fi
  local stylefiles
  stylefiles="$(find "${shellbase}/styles/" -maxdepth 1 -type f \
      \( -name "${style}" -o -name "${style}.*" -o -name "${style}-*.*" \) \
      -printf '%f\n' 2>/dev/null | tr "\n" " ")"
  if [[ ${stylefiles} ]]; then
    v "Using style '${style}' consisting of: ${stylefiles}"
  else
    w "No files for the desired style '${style}' were found."
  fi

  v "Finished copying stylesheets. ✅"
}


# copy_files():		Copy all the contents of the files directory in the input directory to the files directory on the poutput directory. Everything will be
#			  copied to the output directory and thus may be accessible publicly if not prevented e.g. by .htaccess files manually.
# Globals used:		$odir; $shellbase
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	"$odir/files/"*
# Returns:		-
copy_files() {
  vv "Copying files..."
  if option_set R; then								# If pathreducer mode in enabled.
    v "Pathrecuder mode is enabled. Please note that directory names and " \
      "filenames from the web site's files/ directory may be changed. " \
      "Links to those files from web pages may have to be changed " \
      "accordingly. If you don't want the files/ directory to be affected " \
      "by the pathreducer, don't use option --reduce-paths/-R when using " \
      "option --files/-f to copy the files/ directory."
    # shellcheck disable=SC2033
    while IFS= read -r -d '' -u 9; do
      mkdir --parents -- "${odir}$(pathreducer "${REPLY#*$shellbase}")" \
        || e "Could not create the directory '${REPLY}' from the files dir."	# Create directory structure with reduced path names.
    done 9< <( find "${shellbase}"/files/ -type d -exec printf '%s\0' {} + )
    while IFS= read -r -d '' -u 9; do
      cp --update --no-preserve=all -- \
        "${REPLY}" "${odir}$(pathreducer "${REPLY#*$shellbase}")" \
        || e "Could not copy the file '${REPLY}' from the files directoryy."	# Copy files with reduced path names.
    done 9< <( find "${shellbase}"/files/ -type f -exec printf '%s\0' {} + )
  else										# If pathreducer mode is not enabled.
    mkdir --parents -- "${odir}/files" || e "files directory not available."
    cp --recursive --update --no-preserve=all -- \
      "${shellbase}"/files "${odir}/" \
      || e "Could not copy the files directory to the output directory."	# Copy the files/ directory normally.
  fi
  call_hook hook_files
  v "Finished copying files and downloads. ✅"
}


# gen_head():		Generates beginning of HTML document up to and including the header element in the body. Called whenever a new HTML file is generated.
# Globals used:		$outfile; $version; $pagetitle; $shellbase; $style; $sitename
# Vars modified:	$pagetitle
# Inputs:		-
# Outputs:		stderr: warning message
# Files written:	$outfile
# Returns:		-
# Called whenever an HTML header is to be generated.
# Variable $pagetitle has to be always set before calling gen_head. Otherwise the variable may contain a previously used string.
# Variable $outfile also has to be set. gen_head assumes that this variable contains the path of the HTML file that is currently generated and empties it!
gen_head() {
  printf '' > "${outfile}"							# Empty the output file.
  o printf '<!DOCTYPE html>\n'
  o printf '<html>\n'
  o printf '  <head>\n'
  o printf '    <meta charset="UTF-8">\n'
  o printf '    <meta name="generated" content="'
  o encode_html_attr "$(date)"
  o printf '">\n'
  o printf '    <meta name="generator" content="SBWG '
  o encode_html_attr "${version}"
  o printf '">\n'
  o printf '    <title>'
  o encode_html "${pagetitle}"
  o printf '</title>\n'
  o printf '    <link rel="alternate" type="application/rss+xml" '
  o printf 'title="RSS Feed for all entries from '
  o encode_html_attr "${sitename}"
  o printf '" href="'
  o pathreducer '/all.rss'
  o printf '" />\n'
  o printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
  shopt -s nocaseglob
  shopt -s nullglob
  local f
#  for f in "${shellbase}/styles/${style}"{.css,-*.css,.Css,-*.Css,.cSs,-*.cSs,.csS,-*.csS,.cSS,-*.cSS,.CSs,-*.CSs,.CsS,-*.CsS,.CSS,-*.CSS}; do			# Include CSS files belonging to this style, going by their filename because mime
  for f in "${shellbase}/styles/${style}"{.css,-*.css}; do			# Include CSS files belonging to this style, going by their filename because mime
    local filename="${f##*/}"							#   type detection is not reliable/not working for CSS with default configuration.
    if [[ -f ${f} ]]; then
      o printf '    <link rel="stylesheet" href="'
      o encode_url "$(pathreducer "/styles/${filename}")"
      o printf '">\n'
    fi
  done
#  for f in "${shellbase}/styles/${style}"{.js,-*.js,.Js,-*.Js,.jS,-*.jS,.JS,-*.JS}; do			# Include JS files belonging to this style.
  for f in "${shellbase}/styles/${style}"{.js,-*.js,}; do			# Include JS files belonging to this style.
    local filename="${f##*/}"
    if [[ -f ${f} ]]; then
      o printf '    <script type="text/javascript" src="'
      o encode_url "$(pathreducer "/styles/${filename}")"
      o printf '"></script>\n'
    fi
  done
  option_set m && o printf "    <style>header,nav,div.entry-title-wrapper{background-color:#$(printf '%x\n' "$(shuf -i 100-255 -n 1)")$(printf '%x\n' "$(shuf -i 100-255 -n 1)")$(printf '%x\n' "$(shuf -i 100-255 -n 1)");}</style>\n"
  call_hook hook_head								# Hook can be used to insert lines in the <head> tag.
  o printf '  </head>\n'
  o printf '  <body id="'
  o encode_html_attr "${outfile#$odir/}"					# Using the path/filename as a unique ID for the page for styling
  o printf '">\n'
  call_hook hook_header_before
  o printf '    <header>\n'
  call_hook hook_sitename_before
  o printf '      <a href="/"><h1>'
  o encode_html "${sitename}"
  o printf '</h1></a>\n'
  call_hook hook_sitename_after
  o printf '    </header>\n'
  call_hook hook_header_after
  pagetitle="${sitename}"							# Set back to default in case $pagetitle it not set before calling gen_head the next time.
}


# above_entry_content(): Add things (notes and stuff) that belong above the entry content, to the outfile. Called right before opening the content div of each entry,
#			  whether it's on an entry page or tagpage.
# Globals used:		$tags; $shellbase; $outfile
# Vars modified:	-
# Inputs:		-
# Outputs:		stderr: warning message
# Files written:	$outfile
# Returns:		-
above_entry_content() {
  local cachefile
  set_cachefile entries "$(pathreducer "above/${entrybasename}")"		# Cache below entry content chunks in that file.
  call_hook hook_above_entry_content
  if [[ -r ${cachefile} ]]; then						# If there already is a cache file for this chunk and the file can be read ...
    vv "    Adding cached chunk above entry '${entrybasename}'."
    o cat "${cachefile}" || w "Could not use chunk cache file '${cachefile}'."	#   ... just use the contents of the existing cache file and warn if not possible.
  else										# If there is no readable cache file for this entry cache existing, generate it.
    vv "    Generating chunk above entry '${entrybasename}'."
    mkdir --parents -- "${cachefile%/*}"					# Make sure that the directory exists so that the cache file can actually be created.
    :>"${cachefile}"								# Make sure that the file exists so it'll be used even if it turns out to stay emoty.
    call_hook hook_above_entry_content_before
    local re
    local retext='a reply to or continuation of'
    local reftext='referencing'
    local replytext='a reply to'
    local updateoftext='an update of'
    for t in re ref reply updateof; do
      local textvar="${t}text"
      re="$(grab_tag "${tags}" "${t}")"
      if [[ -n ${re} ]]; then
        local reentry
        reentry="$(find "${shellbase}/entries/" -name "${re}" -type f -print -quit)"
        if [[ -n ${reentry} ]]; then
          local retags
          retags="$(grab_tags "${reentry}")"||esub
          local retitle
          retitle="$(grab_tag "${retags}" title)"
          c printf '<div class="entry note">This entry is '
          c printf '%s' "${!textvar}"
          c printf ' the entry '
          if option_set a; then							# If option a (single author blog generation) is set.
            local reauthor
            reauthor="$(grab_tag "${retags}" author)"
            if [[ "${reauthor}" != "${desired_author}" ]]; then
              c encode_html "'${retitle}'"
              c printf ', which is not part of this blog.</div>'
            fi
          fi
          if ! option_set a || [[ "${reauthor}" == "${desired_author}" ]]	# If option a (single author blog generation) is not set or the target entry has
          then									#   the right author.
            c printf '<a href="'
            c encode_url "$(pathreducer "/entries/${re}.html")"
            c printf "\"><em>'"
            c encode_html "${retitle}"
            c printf "'</em></a>.</div>"
          fi
        else
          w "The target entry with the name '${re}' as called for in " \
            "'${entrybasename}' was not found. Not including re link to this entry."
        fi
      fi
    done

    local upentries=
    upentries="${updateslist["${entrybasename}"]}"
    while IFS= read -r upentry; do
      if [[ ${upentry} ]]; then
        upfile="$(find "${shellbase}/entries/" \
                 -name "${upentry}" -type f | grep .)"				# Turn that entry name into a full path of where the entry source file is.
        local uptags
        uptags="$(grab_tags "${upfile}")"||esub
        local uptitle
        uptitle="$(grab_tag "${uptags}" title)"
        c printf '<div class="entry note">There is a newer version of this '
        c printf 'entry: <a href="'
        c encode_url "$(pathreducer "/entries/${upentry}.html")"
        c printf "\"><em>'"
        c encode_html "${uptitle}"
        c printf "'</em></a>.</div>"
      fi
    done <<< "${upentries}"

    local note
    note="$(grab_tag "${tags}" note)"						# Only the first note tag of the entry will be used if there are more than one.
    if [[ ${note} ]]; then							# If there is a note tag in the entry source file header of this entry ...
      c printf '<div class="entry note">'
#      c printf '%s' "${note}"							#   ... print the note as a note.
      c encode_html "${note}"							#   ... print the note as a note.
      c printf '</div>'
    fi

    call_hook hook_above_entry_content_after
  fi
}


# below_entry_content(): Add things (attached images and galleries) that belong below the entry content, to the outfile. Called right after closing the content div
#                         of each entry, whether it's on an entry page or tagpage.
# Globals used:		$entry; $shellbase; $outfile
# Vars modified:	-
# Inputs:		-
# Outputs:		stderr: warning message
# Files written:	$outfile
# Returns:		-
below_entry_content() {
#  case "${FUNCNAME[1]}" in							# Check the name of the function from which this function was called.
#    gen_tagpage) local flavour=tagpage; local attimgsize=minis ;;		# If it was called from gen_tagpage, use minis sized thumbnails.
##    gen_tagpage) local attimgsize=smalls ;;					# If it was called from gen_tagpage, use minis sized thumbnails.
#    gen_entry_page) local flavour=entrypage; local attimgsize=smalls ;;		# If it was called from gen_entry_page, use smalls sized thumbnails.
#    *) local galimgsize=minis ;;						# In all other cases, use minis sized thumbnails.
#  esac
  local cachefile
  set_cachefile entries "$(pathreducer "below/${entrybasename}")"		# Cache below entry content chunks in that file.
  call_hook hook_below_entry_content
  if [[ -r ${cachefile} ]]; then						# If there already is a cache file for this chunk and the file can be read ...
    vv "    Adding cached chunk below entry '${entrybasename}'."
    o cat "${cachefile}" || w "Could not use chunk cache file '${cachefile}'."	#   ... just use the contents of the existing cache file and warn if not possible.
  else										# If there is no readable cache file for this entry cache existing, generate it.
    vv "    Generating chunk below entry '${entrybasename}'."
    mkdir --parents -- "${cachefile%/*}"					# Make sure that the directory exists so that the cache file can actually be created.
    :>"${cachefile}"								# Make sure that the file exists so it'll be used even if it turns out to stay emoty.
    call_hook hook_below_entry_content_before
    if has_flag hideatts; then							# If this entry has the hideatts flag in its header
      c printf '<!-- File attachments are disabled for this entry. This does '
      c printf 'not mean that there would be any files attached to this entry '
      c printf 'otherwise. But if there would be any, they would be here. -->'
    else									# If this entry does not have the hideatts flag in its header
      local atts								# Attachements
      atts="$(find "${shellbase}/entries/" \
        \( -name "${entrybasename}.?*" -o -name "${entrybasename}-?*" \) -type f -print)"
      if [[ -n ${atts} ]]; then
        local attslist=
        while IFS= read -r att; do
          local type
          type="$(file --brief --mime -- "${att}")"
          attslist="${attslist}"$'\n'"${type%/*}:${att}"
        done <<< "${atts}"
        attslist="$(printf '%s' "${attslist}"|sort)"
        c printf '<div class="attachements">\n'
        local att
        while IFS= read -r att; do
          [[ -z ${att} ]] && continue						# Skip empty lines.
          att="${att#*:}"
          local attfn="${att##*/}"
          local attfulltype							# The file type of the attachement.
          attfulltype="$(file --brief --mime -- "${att}")"                        # Mime type and encoding
          local attsubtype="${attfulltype%%; *}"                                  # Mime type
          local atttype="${attsubtype%%/*}"					# Just the mime type (no subtype).
          local attsize
          attsize="$(du --human-readable "${att}")"				# This gets the file size but with a tab and the file name after it.
          case "${atttype}" in
            text) : ;;								# Maybe this will be used later to attach text files.
            image) if ! has_flag hideimageatts; then
                     c printf '<span class="'
                     c encode_html_attr "${attsubtype}"
                     c printf ' attachment">\n'
                     c printf '<a href="'
                     c encode_url "$(pathreducer "/entries/images/${attfn}")"
                     c printf '"><img title="'
                     c encode_html_attr "${attfn}"
                     c printf '" src="'
                     c encode_url "$(pathreducer \
                       "/entries/images/smalls/${attfn}")"			# Hardcoding smalls for now cause this chunk will be cached in single flavour anyway.
                     c printf '" /></a>\n</span>\n'
                   fi
                   ;;
            audio) if ! has_flag hideaudioatts; then
                     c printf '<span class="'
                     c encode_html_attr "${attsubtype}"
                     c printf ' attachment">\n'
                     c printf '<span class="filename">'
                     c encode_html "${attfn}"
                     c printf '</span>\n'					# /filename
                     c printf '<span class="player">'
                     c printf '<audio controls>\n' #src="'
                     c printf '<source src="'
                     c encode_url "$(pathreducer "/entries/audios/${attfn}")"
                     c printf '" type="%s">' "${attsubtype}"
                     c printf 'Here would be an audio element if your browser would '
                     c printf 'support it.\n'
#                     c printf '<a href="'
#                     c encode_url "$(pathreducer "/entries/audios/${attfn}")"
#                     c printf '">'
#                     c encode_html "${attfn}"
#                     c printf '</a>\n'
                     c printf '</audio>\n'
                     c printf '</span>\n'					# /player
                     c printf '<span class="download"><a href="'
                     c encode_url "$(pathreducer "/entries/audios/${attfn}")"
                     c printf '">Download</a></span>\n'				# /download
                     c printf '</span>\n'					# /attachment
                   fi
                   ;;
#            video) if ! has_flag hidevideoatts; then
#                     vv "The attachement '${att}' is not being used because video " \
#                       "attachements are not supported, yet."
#                   fi
#                   ;;
            *)     if ! has_flag hideotheratts; then
                     c printf '<span class="'
                     c encode_html_attr "${attsubtype}"
                     c printf ' attachment"> <a href="'
                     c encode_url "$(pathreducer "/entries/${atttype}s/${attfn}")"
                     c printf '">'
                     c encode_html "Download '${attfn}' (${attsize%$'\t'*})"	# Add the file size without the file name in parantheses.
                     c printf '</a></span>'
                   fi
                   ;;
#            *)     w "The attachement '${att}' is not being used because it's of " \
#                   "the unsupported type '${atttype}'."
#                   continue ;;
          esac
          vv "    Adding ${atttype} attachement '${attfn}'."
#        done
        done <<< "${attslist}"
        c printf '</div>'							# /file attachments
      fi
    fi

    local gallerypath="${shellbase}/galleries/${entrybasename}"
    shopt -s nocaseglob
    shopt -u nullglob								# Just to be sure. (It would assume there is a gallery for every post with nullglob enabled.)
    if ls "${gallerypath}" &> /dev/null; then					# If there is a gallery folder for this entry.
#      case "${FUNCNAME[1]}" in							# Check the name of the function from which this function was called.
#        gen_tagpage) local galimgsize=minis ;;					# If it was called from gen_tagpage, use minis sized thumbnails.
#        gen_entry_page) local galimgsize=thumbs ;;				# If it was called from gen_entry_page, use thumbs sized thumbnails.
#        *) local galimgsize=minis ;;						# In all other cases, use minis sized thumbnails.
#      esac
      local galimgsize=thumbs							# Hardcoding this from now on because this chunk will be cached in a single flavour anyway.
      local images=""
      images="$(\ls -t --directory "${gallerypath}"/*.{jpeg,jpg,png} 2> /dev/null)"
      if [[ -n ${images} ]]; then						# If $images is not still empty
        									#   (In other words: if there is at least one file with one of the extensions)
        c printf '<div class="gallery %s">\n' "${galimgsize}"
        c printf '<p><a href="'
        c encode_url "$(pathreducer "/galleries/${entrybasename}.html")"
        c printf '">View Gallery</a></p>'
        local image
        while IFS= read -r image; do
          local img="${image##*/}"
          c printf '<a class="gallery-image %s" href="' "${galimgsize::-1}"
          c encode_url "$(pathreducer "/galleries/${entrybasename}/${img}")"
          c printf '"><img class="%s" src="' "${galimgsize::-1}"
          c encode_url "$(pathreducer "/galleries/${entrybasename}/${galimgsize}/${img}")"
          c printf '"/></a>'
        done <<< "${images}"
        c printf '</div>\n'							# /gallery attachment
      fi
    fi

    if [[ ${author} ]] && [[ ${emails["${author}"]} ]] \
      && ! has_flag nocomment; then						# If there is an author name for this entry and an email address set for that author,
      c printf '<a class="comment-link" href="mailto:'				#   add a mailto link
      c encode_url "${emails["${author}"]}"					#   to that email address
      c printf '?subject='							#   with a
#      c encode_url "Your entry: ${title}"					#   prefilled subject line
      c encode_url "Re: ${title:-Your entry without title}"			#   prefilled subject line
      c printf '&body='								#   and a
#      c encode_url "This is a comment on your entry '${entrybasename}' on ${sitename}."	#   prefilled body.
      c encode_url "Entry URL: ${url}$(pathreducer "entries/${entrybasename}".html)"	#   prefilled body.
#      c printf '">Comment'							# Link text (obviously).
      c printf '">Comment via email'							# Link text (obviously).
      c printf '</a>'
    fi										# /comment link
    call_hook hook_below_entry_content_after
  fi
}


# add_entry_chunk():	CREATE A SINGLE ENTRY HEADER CHUNK AND ADD IT TO THE CURRENT $outfile
#			  This generates the part of an HTML page that is the title-wrapper of a single entry (as it appears on entry pages and tagpages).
#			  gen_entries/gen_tagpage is called first and then calls add_entry_chunk for every entry that needs to be generated.
# Globals used:		$shellbase; $cachedir; $outfile
# Vars modified:	-
# Inputs:		$1: The entry (path relative to entries dir) whose chunk should be generated.
# Outputs:		stdout: messages; stderr: warning messages
# Files written:	$outfile; $cachefile
# Returns:		2 if supplied entry is not a text file, 0 otherwise
add_entry_chunk() {
  local entry="${1}"
  if [[ $(file --brief --mime "${shellbase}/entries/${entry}") != text* ]]	# Check again for individually generated entries.
  then
    w "'${entry}' is not a text file. Not generating it."
    return 2									# Don't generate anything if the file does not seem to be a text file of any sort.
  fi
  local cachefile
  set_cachefile entries "$(pathreducer "title/${1#$shellbase/}")"		# Cache entry chunks in that file.
  if [[ -r ${cachefile} ]]; then						# If there already is a cache file for this entry chunk and the file can be read ...
    vv "    Adding entry title chunk '${entry}'."
    o cat "${cachefile}" || w "Could not use entry chunk cache file '${cachefile}'."	#   ... just use the contents of the existing cache file and warn if not possible.
  else										# If there is no readable cache file for this entry cache existing, generate it.
    vv "    Generating entry title chunk '${entry}'."
    mkdir --parents -- "${cachefile%/*}"					# Make sure that the directory exists so that the cache file can actually be created.
    local tags
    tags="$(grab_tags "${shellbase}/entries/${entry}")"||esub
    c printf '<div class="entry-title-wrapper">\n'				# This is just to enable styling.
    call_hook hook_entry_title_start
    local created
    created="$(grab_tag "${tags}" created)"
    local edited
    edited="$(grab_tag "${tags}" edited)"
    local entrybasename="${entry##*/}"
    c printf '<a href="'
    c encode_url "$(pathreducer "/entries/${entrybasename}.html")"
    c printf '"><h3 class="entry-title">'
    c encode_html "${title}"
    c printf '</h3></a>\n'
    c printf '<div class="page-info">\n'					# This is just to enable styling.
    # Add last modified date of the entry, link to entry so that entries without titles can be clicked
    c printf '<a class="date" href="'
    c encode_url "$(pathreducer "/entries/${entrybasename}.html")"
#    c printf '">'
    c printf '">Entry '
    c encode_html "${created:+created on }${created:-Permalink }"		# Add created date or "Permalink" if no created date is defined.
    c encode_html " ${edited:+(edited $edited)}"
    c printf '</a>\n'								# closing created/edited line
    call_hook hook_entry_title_tags_before
    local tag
    while IFS= read -r tag; do
      call_hook hook_entry_title_tag_before
      case "${tag}" in (author:*|cat:*|top:*|lang:*)				# If the tag starts with either of those (the tag types that visitors should be
										#   able to view entries by, print a list of tags (or their tagicons if existing)
										#   for this entry, linked to the corresponding tagpage.
        local tagicon
        tagicon="$(find "${shellbase}/tagicons/" \
                   -maxdepth 1 -name "${tag}.?*" -type f -print -quit)"		# See whether there is a file for this tag in the tagicons directory.
        if [[ $(file --brief --mime "${tagicon}") = image/* ]]; then		# If a tagicon image file exists for this tag, use the image instead of the tag name.
          c printf '<span class="tag icon '
          c encode_html_attr "${tag}"
          c printf '">'
          c printf '<a href="'
          c encode_url "$(pathreducer "/tags/${tag}.html")"
          c printf "\" title=\"Show all entries tagged with '"
          c encode_html_attr "${tag}"
          c printf "'.\">"
          c printf '<img src="'
          c encode_url "$(pathreducer "/tags/${tagicon##*/}")"
          c printf '" /></a></span>\n'
        else
          c printf '<span class="tag '
          c encode_html_attr "${tag}"
          c printf '">'
          c printf '<a href="'
          c encode_url "$(pathreducer "/tags/${tag}.html")"
          c printf '">'
          c encode_html "${tag}"
          c printf '</a>'
          c printf '</span>\n'
        fi
      call_hook hook_entry_title_tag_after
      ;;
      esac
    done <<< "${tags}"
    call_hook hook_entry_title_tags_after
    c printf '</div>\n'								# /page-info
    c printf '</div>\n'								# /title-wrapper
  fi
  return 0
}


# gen_entry_page():	CREATE A SINGLE ENTRY PAGE
#			  This generates the HTML page for an entry and does not care about tagpage generation or what tagpages exist.
#			  gen_entries is called first and then calls gen_entry_page for every entry that is to be generated.
# Globals used:		$shellbase; $outfile
# Vars modified:	-
# Inputs:		$1: The entry (path relative to entries dir) whose page should be generated.
# Outputs:		stdout: messages; stderr: warning messages
# Files written:	$outfile
# Returns:		2 if supplied entry is not a text file, 0 otherwise
gen_entry_page() {
  local entry="${1}"
  if [[ $(file --brief --mime "${shellbase}/entries/${entry}") != text* ]]	# Check again for individually generated entries.
    then
    w "'${entry}' is not a text file. Not generating it."
    return 2									# Don't generate anything if the file does not seem to be a text file of any sort.
  fi
  local outfile="${odir}/$(pathreducer "entries/${entry##*/}.html")"		# The HTML file that this entry will be generated into. Using basename because
										#   subdirectories of the source file structure are not used in the resulting URLs.
  if [[ -f ${outfile} ]]; then							# If the file already exists ...
    [[ -w ${outfile} ]] || e "Can not write to ${outfile}. " \
      "Please get your permission straight or report this if you think " \
      "it's a bug. Thank you very much!"
  else										#   ... otherwise check if its directory is writable.
    [[ -w ${outfile%/*} ]] || e "Can not write to ${outfile}. " \
      "Please get your permission straight or report this if you think " \
      "it's a bug. Thank you very much!"
  fi
  local tags
  tags="$(grab_tags "${shellbase}/entries/${entry}")"||esub
  local author
  author="$(grab_tag "${tags}" author)"						# Get the author tag (if there is one).
  if [[ ${desired_author} ]] && [[ ${author} != "${desired_author}" ]]; then	# If option -a was used and this is not an entry by that author.
    return									# Don't continue with this entry.
  fi
  vv "- Generating entry '${entry}'."
  local flags
  flags="$(grab_tag "${tags}" flags)"
  local atts									# Attachements
  atts="$(find "${shellbase}/entries/" \
    \( -name "${entry##*/}.?*" -o -name "${entry##*/}-?*" \) -type f -print)"	# Find any attachements belonging to this entry (files whose name start with the
										#   name of the currently processed entry followed by a file name suffix (extension)
										#   or dash and something.
  if [[ -n ${atts} ]]; then
    vv "    Attachement(s) found for '${entry}'."
    local att									# Single attachement
    while IFS= read -r att; do
      local attfn="${att##*/}"							# The filename of the directory wothout the rest of the path.
      local atttype								# The file type of the attachement.
      atttype="$(file --brief --mime -- "${att}")"
      atttype="${atttype%%/*}"							# Just the mime type (no subtype).
      case "${atttype}" in
        text) : ;;								# Maybe this will be used later to attach text files.
        image) mkdir --parents -- \
                 "${odir}$(pathreducer "/entries/images/smalls")"
               mkdir --parents -- \
                 "${odir}$(pathreducer "/entries/images/minis")"
               convert "${att}" -resize "${smallsize}"x"${smallsize}" \
                 "${odir}/$(pathreducer "entries/images/smalls/${attfn}")" \
                 || w "There was a problem converting '${att}' to " \
                 "'${odir}/$(pathreducer "entries/images/smalls/${attfn}")'."	# Create the "small" (thumbnail) for the image attachement.
               convert  "${att}" -resize "${minisize}"x"${minisize}" \
                 "${odir}/$(pathreducer "entries/images/minis/${attfn}")" \
                 || w "There was a problem converting '${att}' to " \
                 "'${odir}/$(pathreducer "entries/images/minis/${attfn}")'."	# Create the "mini" (thumbnail) for the image attachement.
               ;;
        audio) : ;;								# Here different audio format versions could be created.
        video) : ;;								# Different video formats and sizes could be generated. But that's for later.
        *) : ;;
      esac
      mkdir --parents -- "${odir}$(pathreducer "/entries/${atttype}s")" \
        || w "Could not create directory for entry pictures: " \
        "'${odir}$(pathreducer "/entries/images/smalls")'."
      cp --update --no-preserve=all -- "${att}" \
        "${odir}/$(pathreducer "/entries/${atttype}s/${attfn}")"
      vv "    Added ${atttype} file '${attfn}' to be used as an attachment."
    done <<< "${atts}"
  fi
  call_hook hook_entry_start
  if ! option_set '<'; then							# If the option to ignore redirect tags is not enabled.
    local target
    target="$(grab_tag "${tags}" redirect)"					# Check if there is an entry to which this entry should redirect. ...
    local targetpath
    targetpath="$(find "${shellbase}/entries/" -name "${target}" -type f | grep .)"	# Turn that entry name into a full path of where the entry source file is.
    if [[ -n ${targetpath} ]]; then						# If there is a redirect tag and the target entry is an existing file.
      vv "    Creating redirection to '${target}'."
      redirect \
        "$(pathreducer "entries/${entry##*/}.html")" \
        "$(pathreducer "entries/${target}.html")"				#   ... If so, redirect the page ...
      return 0									#   ... and don't generate anything into this file.
    fi
  fi
  local title
  title="$(grab_tag "${tags}" title)"
  if [[ ${title} ]]; then							# print header
    pagetitle="${title} | ${sitename}"
  else
    pagetitle="(Entry without title) | ${sitename}"
  fi
  gen_head
  add_navbar									# print side bar
  o printf '<main id="entry" '							# This is just to enable styling.
  o printf 'class="'
  o encode_html_attr "$(printf '%s' "${tags}" \
    | grep --text --extended-regexp -- \
    '^cat:|^top:|^lang:|^author:|^note:|^flags:' | tr " " "-")"			# This is just to enable styling.
  o printf '">\n'								# This is just to enable styling.
  add_entry_chunk "${entry}"
  local entrybasename="${entry##*/}"
  above_entry_content								# Add reply, ref and re tags
  o printf '<div id="content">\n'						# This is just to enable styling.
  call_hook hook_entry_before
  add_content "${shellbase}/entries/${entry}"
  call_hook hook_entry_after
  o printf '</div>\n'								# /content
  below_entry_content
  o printf '</main>\n'
  o printf '</section>\n'							# This was just to enable styling.
  add_footer
  call_hook hook_entry_end
  return 0
}


# gen_entries():	CREATE ENTRIES
#			  Is called once if entries are to be generated in this run. gen_entry_page() will then be called for each entry that is to be generated.
# Globals used:		$odir; $desired_entry_name; $desired_entry_file; $entrylist
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: error message
# Files written:	-
# Returns:		-
gen_entries() {
  vv "Generating entries..."
  mkdir --parents -- "${odir}/entries" || e "entries directory not available."
  call_hook hook_entries_start

  [[ -n ${desired_entry_name} ]] \
    && gen_entry_page "${desired_entry_full#$shellbase/entries/}"		# Generate only the entry passed as an argument to option -e
										#   (the filename found by the find command from the if line in the prepare
										#   function but without the beginnging of the path)
  [[ -n ${desired_entry_file} ]] \
    && gen_entry_page "${desired_entry_file#$shellbase/entries/}"		# Generate only the entry passed as an argument to option -E

  if [[ -z ${desired_entry_name} ]] && [[ -z ${desired_entry_file} ]]; then	# If neither option -e nor -E was given an argument, generate all entries.
    local entry
    for entry in "${entrylist[@]}"; do						# Look into each entry again.
      p gen_entry_page "${entry#$shellbase/entries/}"				# Generate entrypages.
    done
  wait_for_p									# Wait for any background processes from parallel generation to finish.
  fi
  call_hook hook_entries_end
  v "Finished generating entries. ✅"
}


# gen_gallery():	CREATE SINGLE GALLERY
#			  This generates the HTML file for a singe gallery. gen_galleries is called first and then calls gen_gallery for every gallery that is to
#			  be generated.
# Globals used:		$shellbase; $odir; $outfile
# Vars modified:	-
# Inputs:		$1: Gallery name
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	$outfile
# Returns:		-
gen_gallery() {
  local gallery="${1}"
  [[ -z ${gallery}  ]] && return 2						# Stop right here if no gal name was passed. No use in trying to generate nothing.
  if option_set v; then
    waiting_ani_stop
    printf '   - %s ' "${gallery}"
    if ! option_set_multi v; then						# This newline is only included if not in very verbose mode because no dots and ...
      printf '\n'								#   ... colons will be printed in the same line but the next gallery name (or ...
    fi										#   ... output from the next task) will come next. If in very verbose mode the ...
    waiting_ani_start								#   ... newline will be printed after the dots and colons, towards end of function.
  fi
  mkdir --parents -- "${odir}/$(pathreducer "galleries/${gallery}")" \
    || e "Gallery directory '$(pathreducer "${gallery}")' not available."	# Create the gallery directory in the output directory before copying any files.
  local imagefile
  for imagefile in "${shellbase}/galleries/${gallery}"/*; do
    if option_set F \
    || [[ $(file --brief --mime "${imagefile}") == image* ]]; then      	# If the file is indeed some image file (regardless of filename) ...
      cp --update --no-preserve=all -- "${imagefile}" \
        "${odir}/$(pathreducer "galleries/${gallery}/${imagefile##*/}")" \
        || e "Could not copy '${imagefile}' to " \
        "'${odir}/$(pathreducer "galleries/${gallery}")/'. Aborting."		#   ... then copy the original to the output gallery directory.
    else
      w ""; w "${imagefile} does not seem to be an image file. I'll ignore it." # Note that the convertation to various image sizes is handled separately below.
    fi
  done

  if [[ $(\ls -A "${gallerypath}") ]]; then					# If $images is not still empty ...
        									#   ... (In other words: if there is at least one file with one of the extensions)
    local entry
    entry="$(find "${shellbase}/entries/" -name "${gallery}" -type f | grep .)"
    if [[ -s ${entry} ]]; then							# If a corresponding blog entry exists
      local tags
      tags="$(grab_tags "${entry}")"||esub
      local title
      title="$(grab_tag "${tags}" title)"
      pagetitle="${title} | ${sitename}"
    else
      pagetitle="${gallery} | ${sitename}"
    fi
    outfile="${odir}/$(pathreducer "galleries/${gallery}.html")"
    if [[ -f ${outfile} ]]; then						# If the file already exists, check if it is writable ...
      [[ -w ${outfile} ]] || e "Can not write to '${outfile}'. " \
        "Please check your permission or report this if you think may be a " \
        "bug. Thank you!"
    else									#   ... otherwise check if its directory is writable.
      [[ -w ${outfile%/*} ]] || e "Can not write to '${outfile}'. " \
        "Please check your permission or report this if you think may be a " \
        "bug. Thank you!"
    fi
    call_hook hook_gallery_start
    gen_head
    add_navbar
    o printf '<main id="gallery">\n'						# This is to enable styling for gallery pages.
    if [[ -s ${entry} ]]; then							# If a corresponding blog entry exists
      o printf '<a name="top"><h2 id="page-title">'
      o encode_html "${title}"
      o printf '</h2></a>\n'							# Print title of the blog entry because it's neater than the gallery directory name.
      o printf '<p><a title="There is a blog entry appertaining to this gal'
      o printf 'lery. Click here to go to the blog entry." href="'
      o encode_url "$(pathreducer "/entries/${gallery}.html")"
      o printf '">View corresponding blog entry</a></p>\n'			# Also link to that blog entry.
    else									# If no correstponding blog entry exists
      o printf '<a name="top"><h2 id="page-title">Image '
#      o printf 'Gallery: %s</h2></a>\n' "${gallery}"				# Print a title based on the gallery directory name.
      o printf 'Gallery: '
      o encode_html "${gallery}"						# Print a title based on the gallery directory name.
      o printf '</h2></a>\n'
    fi
    o printf '<div id="gallery-minis">\n'

    mkdir --parents -- "${odir}/$(pathreducer "galleries/${gallery}")/minis/"	# Make sure the subdirectory for this image size exists in this gallery directory.
    mkdir --parents -- "${odir}/$(pathreducer "galleries/${gallery}")/thumbs/"
    mkdir --parents -- "${odir}/$(pathreducer "galleries/${gallery}")/previews/"
    local image
    for image in "${gallerypath}"/*; do						# Create thumbnails and other sizes for all the gallery images.
      if [[ -d ${image} ]]; then
        vv "Skipping directory '${image}' because it's not an image file."
        continue
      else									# If $image is not a directory...
        [[ -f ${image} ]] \
          || e "Won't create image sizes: '${image}' is not an existing file."	#   ... and not a regular file, error out.
      fi
      if ! [[ $(file --brief --mime "${image}") == image* ]] \
        && ! option_set F; then                                         	# If this file is not an image file and force mode is not enabled ...
        vv "Skipping '${image}' because it doesn't appear to be an image file."
        continue								#   ... skip it.
      fi
      local img="${image##*/}"
      local imgdir
      for imgdir in minis thumbs previews; do
        if option_set F || \
          [[ ! -f ${odir}/$(pathreducer "galleries/${gallery}/${imgdir}/${img}") ]]	# If the resized images doesn't exist yet or force mode is enabled.
          then
          local sizevar=${imgdir}ize						# For dynamic variable name (thumbsize, minisize, previewsize).
          convert "${image}" -resize "${!sizevar}"x"${!sizevar}" \
            "${odir}/$(pathreducer "galleries/${gallery}/${imgdir}/${img}")" \
            || w "There was a problem converting '${image}' to " \
            "'${odir}/$(pathreducer "galleries/${gallery}/${imgdir}/${img}")'."	# Resize the original image.
          if option_set_multi v; then
            waiting_ani_stop
            printf '.'
            waiting_ani_start
          fi
        else									# If this size this image already exists, skip generating it.
          if option_set_multi v; then
            waiting_ani_stop
            printf ','
            waiting_ani_start
          fi									# Print . for a created file and , for a skipped, existing one.
        fi
      done

      o printf '<a class="gallery-image mini" href="#'
      o encode_html_attr "${img}"
      o printf '">'
      o printf '<img src="'
      o encode_url "$(pathreducer "/galleries/${gallery}/minis/${img}")"
      o printf '"/></a>\n'
    done

    o printf '</div>\n'								# /gallery-minis
    call_hook hook_gallery_before
    o printf '<div id="gallery-previews">\n'
    local image
    for image in "${gallerypath}"/*; do
      if [[ -d ${image} ]]; then
        vv "Skipping directory '${image}' because it's not an image file."
        continue
      else									# If $image is not a directory...
        [[ -f ${image} ]] \
          || e "Won't create gallery page: '${image}' is not an existing file."	#   ... and not a regular file, error out.
      fi
      local img="${image##*/}"
      call_hook hook_gallery_image_before
      o printf '<br />\n'
      o printf '<a name="'
      o encode_html_attr "${img}"
      o printf '" class="gallery-image preview" href="'
      o encode_url "$(pathreducer "/galleries/${gallery}/${img}")"
      o printf '"><img src="'
      o encode_url "$(pathreducer "/galleries/${gallery}/previews/${img}")"
      o printf '"/></a>'
      o printf '<a title="Do clickery here to go to the top of the gallery '
      o printf 'page." href="#top" class="gototop"> Go to top ⬆</a>'
      o printf '<br />\n'
      call_hook hook_gallery_image_after
    done
    o printf '</div>\n'								# /gallery-previews
    call_hook hook_gallery_after
    o printf '</main>\n'							# /gallery
    o printf '</section>\n'							# This was just to enable styling.
    add_footer
  else										# If there is no file with one of the supported extensions in the directory
    w "does not contain any supported image files."
  fi
  if option_set_multi v; then							# This newline is only printed if in very verbose mode because otherwise it has ...
    waiting_ani_stop								#   ... already been printed above, before any dots and colons have been ...
    printf '\n'									#   ... printed so that it doesn't get into the way of the waiting animation.
    waiting_ani_start
  fi
  call_hook hook_gallery_end
}


# gen_galleries():	CREATE IMAGE GALLERY PAGES
# Globals used:		$desired_gallery; $odir; $shellbase
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: error message
# Files written:	-
# Returns:		-
gen_galleries() {
  if [[ -z ${desired_gallery} ]] \
    && ! \ls -A "${shellbase}"/galleries/* &> /dev/null; then			# If there is nothing in the galleries/ directory and no desired_gallery ...
    return 1									# Skip this function, don't attempt to create any galleries.
  fi
  mkdir --parents -- "${odir}/galleries" \
    || e "galleries directory not available."
  v "Generating various image sizes of galleries:"
  vv ". = Generated one file			, = Skipped an existing file"
  call_hook hook_galleries_start
  if [[ -n ${desired_gallery} ]]; then						# If the option to (re-)generate a single gallery is used ...
    if [[ -d ${shellbase}/galleries/${desired_gallery} ]]; then
      local gallerypath="${shellbase}/galleries/${desired_gallery}/"
      gen_gallery "${desired_gallery}"						#   ... generate just this single gallery ...
    else
      e "The desired gallery '${desired_gallery}'" \
        "does not exist in the galleries directory."
    fi
  else
    shopt -s nocaseglob								# Should already be set, but to be save, set it again. (Because .jpg/.JPG etc.)
    local gallerypath
    for gallerypath in "${shellbase}"/galleries/*/; do
      if [[ ${gallerypath: -1} == / ]]; then
        gallerypath="${gallerypath::-1}";					# Remove trailing slash if there was one so we can be sure what we have.
      fi
      local gallery="${gallerypath##*/}"
      p gen_gallery "${gallery}"						# Generate the gallary pages.
    done
    wait_for_p									# Wait for any background processes from parallel generation to finish.
  fi
  call_hook hook_galleries_end
  v "Finished generating image gallery pages. ✅"
}


# gen_page():		CREATE SINGLE PAGE
#			  This generated the HTML file for a single page source file (from the source directory/pages/).
#			  gen_pages is called first and then calls gen_pages for every page that is to be generated.
# Globals used:		$shellbase; $odir
# Vars modified:	-
# Inputs:		$1: Name of the page that should be generated
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	$outfile
# Returns:		2 if supplied page is not a text file, 0 otherwise
gen_page() {
  local page="${1}"
  vv "- Generating page '${page}'."
  if ! option_set F \
    && [[ $(file --brief --mime "${shellbase}/pages/${page}") != text* ]]
  then										# Check again for individually generated pages.
    w "'${page}' is not a text file. Not generating it. You may use --force."
    return 2									# Don't generate anything if the file does not seem to be a text file of any sort.
  fi
  local outfile
  outfile="${odir}/$(pathreducer "${page}.html")"		        	# The HTML file that this page will be generated into.
  if [[ ${page} == */?* ]]; then                                                # If the page is in a subdirectory.
    mkdir --parents "${odir}/${page%/*}/" \
      || e "Directory for page '${page}' not available."                        # Create subdirectory if it doesn't already exit.
  fi
  if [[ -f ${outfile} ]]; then							# If the file already exists, check if it is writable ...
    [[ -w ${outfile} ]] || e "Can not write to ${outfile}. " \
      "Are you sure the directory permission are set correctly?"
  else										#   ... otherwise check if its directory is writable.
    [[ -w ${outfile%/*} ]] || e "Can not write to ${outfile}. " \
      "Are you sure the directory permission are set correctly?"
  fi
  local tags
  tags="$(grab_tags "${shellbase}/pages/${page}")"||esub
  local title
  title="$(grab_tag "${tags}" title)"						# The title is used as <title> tag and printed as <h2> in the body
  call_hook hook_page_start
  if [[ -n ${title} ]]; then							# If the page has a title tag that is not empty in its source file header ...
    pagetitle="${title} | ${sitename}"						#   ... then include that title in the HTML page title.
  else										# If there is no title tag or it's empty ...
    pagetitle="${sitename}"							#   ... then use just the site name as the HTML page title.
  fi
  gen_head
  add_navbar
  o printf '<main id="page">\n'							# This is just to enable styling.
  if [[ -n ${title} ]]; then							# If the page has a title tag that is not empty in its source file header ...
    call_hook hook_page_title_before
    o printf '<h2 id="page-title">'
    o encode_html "${title}"
    o printf '</h2>\n'								#   ... print that tile as h2 heading.
    call_hook hook_page_title_after
  fi
  call_hook hook_page_content_before
  o printf '<div id="content">\n'						# This is just to enable styling.
    local note
    note="$(grab_tag "${tags}" note)"						# Only the first note tag of the entry will be used if there are more than one.
    if [[ ${note} ]]; then							# If there is a note tag in the entry source file header of this entry ...
      o printf '<div class="page note">'
      o encode_html "${note}"							#   ... print the note as a note.
      o printf '</div>'
    fi
  add_content "${shellbase}/pages/${page}"
  o printf '</div>\n'								# /content
  call_hook hook_page_content_after
  o printf '</main>\n'
  o printf '</section>\n'							# This was just to enable styling.
  add_footer
  call_hook hook_page_end
  return 0
}


# gen_pages():		CREATE PAGES
# Globals used:		$desired_page_name; $desired_page_full; $desired_page_file; $shellbase
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: error message
# Files written:	-
# Returns:		-
gen_pages() {
  vv "Generating pages..."

  call_hook hook_pages_start

  [[ -n ${desired_page_name} ]] \
    && gen_page "${desired_page_full#$shellbase/pages/}"			# Generate only the page passed as an argument to option -p
										#   (the filename found by the find command from the if line in the prepare
										#   but without the function beginnging of the path)
  [[ -n ${desired_page_file} ]] \
    && gen_page "${desired_page_file#$shellbase/pages/}"			# Generate only the page passed as an argument to option -P

  if [[ -z ${desired_page_name} ]] && [[ -z ${desired_page_file} ]]; then	# If neither option -P nor -p was given an argument, generate all pages.
    local page
    for page in "${shellbase}"/pages/**; do
      [[ -d ${page} ]] && continue						# Don't use directories, only the files in them.
      [[ -f ${page} ]] \
        || e "${page} is not an existing file."
      p gen_page "${page#$shellbase/pages/}"					# Generate the pages.
    done
    wait_for_p									# Wait for any background processes from parallel generation to finish.
  fi
  call_hook hook_pages_end
  v "Finished generating pages. ✅"
}


# gen_navbar():		CREATE navbar
#			  This function looks for all existing tags in all entries and generates a sorted list of those tags, linked to their corrosponding tagpages.
#			  Below that a sorted list of all galleries is placed, with each gallery name linked to its gallery page. Below that there are currently
#			  hardcoded other links, which may in future be replaced by a news tag type "menu:" in page source files and possibly entry source files.
#			  Currently menu entries and other HTML code can be inserted into the side bar using the hook_navbar() function in the settings file.
# Globals used:		$shellbase; $desired_author; $entrylist; $tagslist_unsorted; $tagslist
# Vars modified:	$tagslist_unsorted; $tagslist_unsorted; $tagslist
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning messages
# Files written:	$outfile ($cachedir/navbar)
# Returns:		-
gen_navbar() {
  vv "Generating tagslist..."
  entrylist=()									# Start with empty array to store the entry names in.
  local entry
  for entry in "${shellbase}"/entries/**; do
    [[ -d ${entry} ]] && continue						# Don't use directories, only the files in them.
    local filetype
    filetype="$(file --brief --mime-type "${entry}")"				# Determine mime type of the supposed entry source file.
    case "${filetype}" in
#      image/*|video/*|audio/*) : ;;						# Don't use media files as entries.
      text/*) entrylist+=("${entry}") ;;					# Use text files as entries.
#      *) if option_set F
#         then
#            entrylist+=("${entry}")						# Attempt to use other files if force mode is enabled.
#            w "I was forced to use ${entry} as an entry despite it not " \
#              "looking like a text file."
#        else
#          w "'${entry}' is not a text file. Not using it. You may use --force."
#        fi ;;
    esac
  done

  declare -a tagslist_unsorted=()						# Start with empty array
  call_hook hook_navbar_start
  local -i num=0								# Start at item 0 in the array.

  process_this_entry() {							# Put this block into a function so it can be called in parallel or not as needed.
    local tags
    tags="$(grab_tags "${entry}")"||esub
    local author
    author="$(grab_tag "${tags}" author)"
    if [[ ${desired_author} ]] && [[ ${author} != "${desired_author}" ]]; then	# If option -a was used and this is not an entry by that author.
      return									# Don't include this entry.
    fi
    local tag
    while IFS= read -r tag; do							# For each tag that's been found in this entry.
      tagslist_unsorted[${num}]="${tag}"					#   put the tag as a new item into the array.
      if [[ ${tag} == top:* ]] || [[ ${tag} == *+top:* ]]; then			# Only topic tags should have their sub-tags treated.
        while [[ ${tag} == *:*:* ]]; do						# If the tag line contains two colons, it's a sub-tag (subtops).
          tag="${tag%:*}"							# To make sure all parents of existing sub-tags are also listed in the navbar ...
          (( num+=1 ))								#   ... cut off the child to make the currect tag the parent tag and ...
          tagslist_unsorted[${num}]="${tag}"		        		#   ... put the tag as a new item into the array.
        done
      fi
      (( num+=1 ))								# Next time next item.
    done <<< "${tags}"
    local updateof
    updateof="$(grab_tag "${tags}" updateof)"
    if [[ ${updateof} ]]; then							# If there is a updateof tag among the tags of the currently processed entry.
      updateslist["${updateof}"]="${updateslist["${updateof}"]}"$'\n'"${entry##*/}"	# Add the name of the current entry to the list of entries to which the current ...
										#   ... entry is an update of.
    fi
  }

  for entry in "${entrylist[@]}"; do						# Look into each entry
    p process_this_entry							# Process the entries sequentially.
  done
  wait_for_p									# Wait for any background processes from parallel generation to finish.
  readarray -t tagslist < <(printf '%s\n' "${tagslist_unsorted[@]}"|sort \
    --field-separator=: -k1r,1 -k2,2|uniq)					# Put unsorted tagslist sorted into array tagslist, one item per line.

  if option_set a && ! tag_exists "author:${desired_author}"; then		# If the desired author is not in the array of existing tags.
    e "No entry by the author '${desired_author}' exists in this web site."	# If desired author doesn't exist: abort.
  fi

  v "Finished generating tagslist. ✅"


  local firstcat=false								# Turns true when the first category tag has been found in the below loop.
  local firstlang=false								# Turns true when the first language tag has been found in the below loop.
  local firsttop=false								# Turns true when the first topic tag has been found in the below loop.
  local firstauthor=false							# Turns true when the first author tag has been found in the below loop.
  local outfile
  local cachefile
  outfile="${odir}/$(pathreducer navbar.html)"
  gen_head
  set_cachefile navbar
  if [[ -r ${cachefile} ]]; then						# If there already is a cached navbar file and the file can be read ...
    vv "Using cached navbar."
    o cat "${cachefile}" || w "Could not use cached file file '${cachefile}'."	#   ... just use the contents of the existing navbar file and warn if not possible.
  else										# If there is no readable cached navbar file, generate it.
    vv "Generating navbar..."
    # Turn the sorted list into HTML code for the navbar
    c printf '<nav id="navbar">\n'
    call_hook hook_navbar_before
    if [[ -n ${desired_entry_file} ]] \
      || \ls -A "${shellbase}"/entries/* &> /dev/null; then			# If there is something in the entries/ directory or an entry from somewhere else
										#   was requested.
      c printf '<h3 class="navheading"><a title="A page listing all blog '
      c printf 'entries regardless of language, author, category and topic '
      c printf 'in full." href="'
      c pathreducer /all.html
      c printf '">Weblog</a></h3>\n'
      local oddity=odd
      local item
      local -i i
      for (( i=0; i <= ${#tagslist[@]}; i++ )); do
        item="${tagslist[${i}]}"
        call_hook hook_navbar_item_before
        case ${item} in
          cat:*)		       						# If the item starts with "cat"
            if ! ${firstcat}; then						# Add heading before first occurance
              firstcat=true
              c printf '<div class="navheading" id="cat"><h3>Blog Categories'
              c printf '</h3><ul>\n'
            fi
            local tag
            tag="$(printf '%s' "${item}" | cut --characters=5-)"
            c printf '<li class="cat %s">' "${oddity}"
            c printf "<a title=\"Show blog entries in the category '"
            c encode_html_attr "${item}"
            c printf "'.\" href=\""
            c encode_url "$(pathreducer "/tags/${item}.html")"
            c printf '">'
            c encode_html "${tag}"
            c printf '</a></li>\n'
            if [[ ${oddity} == odd ]]; then oddity=even; else oddity=odd; fi	# Alternate between odd and even for CSS class.
            ;;
          top:*)								# If the item starts with "top"
            if ! ${firsttop}; then						# Add heading before first occurance
              firsttop=true
              c printf '<div class="navheading" id="top"><h3><a href="'
              c pathreducer /tags/top.html
              c printf '" title="List all entries '
              c printf 'that a topic is assigned to.">Topics</a></h3><ul>\n'
            fi
            local tag
            tag="$(printf '%s' "${item}" | cut --characters=5-)"
            c printf "<li class=\""
            while [[ ${tag} == *:* ]]; do					# If the tag still contains a colon (meaning it is a subtopic)
              c printf "sub"
              tag="${tag#*:}"
            done
            c printf "top %s\"><a title=\"List blog entries " "${oddity}"
            c printf "regarding the topic '"
            c encode_html_attr "${item}"
            c printf "'.\" href=\""
            c encode_url "$(pathreducer "/tags/${item}.html")"
            c printf "\">"
            c encode_html "${tag}"
            c printf "</a></li>\n"
            if [[ ${oddity} == odd ]]; then oddity=even; else oddity=odd; fi	# Alternate between odd and even for CSS class.
            ;;
          lang:*)								# If the item starts with "lang"
            if ! ${firstlang}; then						# Add heading before first occurance
              firstlang=true
              c printf '<div class="navheading" id="lang"><h3>By Language'
              c printf '</h3><ul>\n'
            fi
            local tag
            tag="$(printf '%s' "${item}" | cut --characters=6-)"
            c printf "<li class=\"lang %s\"><a title=\"Show blog en" "${oddity}"
            c printf "tries with the language tag '"
            c encode_html_attr "${item}"
            c printf "'.\" href=\""
            c encode_url "$(pathreducer "/tags/${item}.html")"
            c printf "\">"
            c encode_html "${tag}"
            c printf "</a></li>\n"
            if [[ ${oddity} == odd ]]; then oddity=even; else oddity=odd; fi	# Alternate between odd and even for CSS class.
            ;;
          author:*)								# If the item starts with "author"
            if [[ ! ${desired_author} ]]; then	        			# Only include this menu item if option -a was not used
              if ! ${firstauthor}; then						# Add heading before first occurance
                firstauthor=true
                c printf '<div class="navheading" id="author"><h3>By Author'
                c printf '</h3><ul>\n'
              fi
              local tag
              tag="$(printf '%s' "${item}" | cut --characters=8-)"
              c printf "<li class=\"author %s\"><a title=\"" "${oddity}"
              c printf "Show blog entries supposedly written by '"
              c encode_html_attr "${item}"
              c printf "'.\" "
              c printf "href=\""
              c encode_url "$(pathreducer "/tags/${item}.html")"
              c printf "\">"
              c encode_html "${tag}"
              c printf "</a></li>\n"
              if [[ ${oddity} == odd ]]; then oddity=even; else oddity=odd; fi	# Alternate between odd and even for CSS class.
            else
              continue								# Just don't do anything of the following with author tags if option -a was used.
            fi
            ;;
          *)									# Every other item
            ;;
        esac
        call_hook hook_navbar_item_after
        if [[ ${item%%:*} != "${tagslist[${i}+1]%%:*}" ]]; then			# If the next item in the tagslist is of a different tag type than the current one.
          case ${item} in
           top:*|cat:*|lang:*|author:*)						# If the current item is of one of the tag types that are listed in the navbar.
              call_hook hook_navbar_item_change
              c printf "</ul></div>\n"						# /.navheading
            ;;
          esac
        fi
      done
    fi

    if [[ -n ${desired_gallery} ]] \
      || \ls -Ad "${shellbase}"/galleries/* &> /dev/null; then			# If there is something in the galleries/ directory or there is a desired_gallery ...
      c printf "<div class=\"navheading\" id=\"galleries\"><h3>Galleries</h3><ul>\n"
      gallerylist=()
      for gallery in "${shellbase}"/galleries/*/; do
        gallery="${gallery::-1}"                                                  # The traiing slash has to be cut off for the improvised bash builtin basename
                                                                                #   simple substitute that is used below to work.
        local has_pics=false
        for pic in "${gallery}"/*; do						# Check each file in the the gallery directory.
          if [[ $(file --brief --mime "${pic}") == image* ]] \
            || option_set F; then                                                 # Accept any file types that start with image or any file if force mode is enabled.
            has_pics=true								# To enable skipping empty and non-picture directories.
            break									# Sometimes it's just good to break free of the loop that is our everyday life.
          fi
        done
        if ! ${has_pics}; then
          w "Gallery directory '${gallery##*/}' contains no images."
        else
          gallerylist+=("${gallery##*/}")
        fi
      done
      call_hook hook_navbar_galleries_before
      for gallery in "${gallerylist[@]}"; do
        call_hook hook_navbar_gallery_before
        if [[ -s ${shellbase}/entries/${gallery} ]]; then				# If a corresponding blog entry exists
          local tags
          tags="$(grab_tags "${shellbase}/entries/${gallery}")"||esub
          local title
          title="$(grab_tag "${tags}" title)"
          local gallerytitle="${title}"
        else
          local gallerytitle="${gallery}"
        fi
        c printf "<li><a title=\"Show what's in the gallery '"
        c encode_html_attr "${gallerytitle}"
        c printf "'.\" href=\""
        c encode_url "$(pathreducer "/galleries/${gallery}.html")"
        c printf "\">"
        c encode_html "${gallerytitle}"
        c printf "</a></li>\n"							# Add each gallery to the list
        call_hook hook_navbar_gallery_after
      done
      call_hook hook_navbar_galleries_after
      c printf "</ul></div>\n"							# /.navheading
    fi
    call_hook hook_navbar_after
    c printf "</nav>\n"
    call_hook hook_navbar_end
  fi
  add_footer
  v "Finished generating navbar file. ✅"
  return 0
}


# add_filters():	Print the tag filter links for the top of tagpages.
# Globals used:		$desired_author; $tag; $entrylists; $perpage; $odir; $outfile
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning messages
# Files written:	$outfile
# Returns:		-
add_filters() {
  if [[ ! ${desired_author} ]] && [[ ${tag} != all ]] \
    && [[ ${tag} != author:* ]]; then						# If this is not all.html and not an author tagpage (or combined author tagpage)
										#   and there was no author set with option -a.
										#   Generate filter links where entries from more than one author are available.
    local secs
    secs="$(printf '%s\n' "${!entrylists[@]}" \
      | grep --text -- ^author: | grep --text --invert-match \
      --extended-regexp '[+]cat:|[+]top:|[+]lang:' )"				# Get all existing authors (no combined tags).
    local sec									# sec meaning the secondary tag that is going to be combined with the current tag.
    local filters								# Will be assigned the string that will be printed to the HTML file
    local -i nums								# Number of second tags
    local -i numf								# Number of filters that are available
    for sec in ${secs}; do							# Cycle through all authors to look for entries with the current tag.
      nums="$(printf '%s' "${entrylists[${sec}+${tag}]}" \
        | grep --text --invert-match --count -- ^$)"				# Number of non-empty lines of entries for the combined tagpage of
										#   this author + this tag.
      (( nums > 0 )) && {
        filters="${filters}<span class=\"filter\"><a title=\""
        filters="${filters}${num} entries from ${sec} tagged with '"
        filters="${filters}$(encode_html_attr "${tag}")"
        filters="${filters}\" href=\"$(encode_url "$(pathreducer \
          "/tags/${sec}+${tag}.html")")\">"
        filters="${filters}$(encode_html "${sec}")</a></span>"
        numf=$(( numf + 1 ))
      }
    done
    (( numf >= 2 )) && {							# Only display the filter links if there are at least two filters available.
      o printf "<div id=\"filters\">\n"						# It is needed for styling.
      o printf '%s' "${filters+<details><summary>Filter further</summary> by $filters</details>}"
      o printf "</div>\n"							# / #filters
    }
  fi
  [[ ${tag} == author:*+*:* ]] && {
    o printf "<div id=\"filters\">\n"						# It is needed for styling.
    o printf '<span class="remove-filter"><a href="'
    o encode_url "$(pathreducer "/tags/${tag#*+}.html")"
    o printf '">Remove Filter</a></span>'					# If this is a combined author tagpage, include a link to the tagpage of the original
    o printf "</div>\n"								# / #filters
  }										#   tag (without the author filter applied).
}


# gen_tagpage():	CREATE SINGLE TAGPAGE
#			  Generates the tagpage for the tag that's passed as an argument. This function is only called from gen_tagpages(), wherethe $entrylists
#			  array is prepared. So gen_tagpages is called first and then calls gen_tagpage for every tagpage that is to be generated, including "all"
#			  and "top".
# Globals used:		$entry; $entrylists; $perpage; $odir; $outfile
# Vars modified:	-
# Inputs:		$1: Tag name (Generate all.html and its pages when no argument is given.)
# Outputs:		stdout: messages; stderr: warning messages
# Files written:	$outfile
# Returns:		-
gen_tagpage() {
  if [[ ${desired_author} ]] && [[ ${tag} == author:* ]]; then			# If the generated web site is reduced to one single author and this would be an
    return									#   author tagpage (or combined author tagpage), skip this tag
  fi
  local tag="${1:-all}"								# Use provided tag name. If unset, use "all", generating special tagpage all.html.
  vv "- Generating tagpage '${tag}'."

  while IFS= read -r entry; do
    local entryname="${entry##*:}"						# Cut off the date/sortby string and the colon from the beginning.
    if [[ -z ${entryname} ]]; then continue; fi					# If the line is empty, stop right there.
    local tags
    tags="$(grab_tags "${shellbase}/entries/${entryname}")"||esub
    local flags=
    target="$(grab_tag "${tags}" flags)"
    if has_flag hide || has_flag noshow; then
#äääääääääääääääää
:
    fi
    local target=
    target="$(grab_tag "${tags}" redirect)"
    if [[ -n ${target} ]] && ! option_set '<' && ! option_set '>'; then
      entrylists[${tag}]="${entrylists[${tag}]/${entry}/}"
#    else
#     echo "${entryname}" > /H/1
    fi
#ööööööööööööööööööööööööööööööööööö

  done <<< "${entrylists[${tag}]}"						# End of while IFS= loop.

  local -i entrycount=-1							# For paging we will count the number of entries processed so far for each tag.
  local -i lastentry
  lastentry=$(( $(printf '%s' "${entrylists[${tag}]}" | uniq | wc --lines) - 2 ))	# The number ob lines in the list of entries for this tag is the number of
  local -i lastpage=0								# Will be used to store the number of the last page that will be generated.
  local -i pagecount=0								#   The first page is page 0.
  lastpage=$(( (( lastentry + (perpage - 1)) / perpage) - 1 )) 			# Determine the number of pages necessary (number of entries devided by $perpage,
										#   always rounded up)
  (( lastpage > 0 )) && [[ ${tag} != top* ]] && [[ ${tag} != *+top:* ]] \
    && vv "   Starting page 0."
  local outfile
  if [[ ${tag} == all* ]]; then
    outfile="${odir}/$(pathreducer "${tag}.html")"
  else
    outfile="${odir}/$(pathreducer "tags/${tag}.html")"
  fi
  if [[ -f ${outfile} ]]; then							# If the file already exists, check if it is writable ...
    [[ -w ${outfile} ]] || e "Can not write to ${outfile}. " \
      "Do you have write permission to that directory? Please check " \
      "and report any bugs if you wish."
  else										#   ... otherwise check if its directory is writable.
    [[ -w ${outfile%/*} ]] || e "Can not write to ${outfile}. " \
      "Do you have write permission to that directory? Please check " \
      "and report any bugs if you wish."
  fi

  call_hook hook_tagpage_start
  if [[ ${tag} == all ]]; then
    local tagname='All Entries'
  else
    local tagname="Entries tagged '${tag}'"					# all.html gets a title, all other tagpages get a title with the tag in it.
  fi
  pagetitle="${tagname} | ${sitename}"
  gen_head
  add_navbar
  o printf '<main id="tagpage">\n'						# This is just to enable styling.
  o printf '<h2 id="page-title">'
  o encode_html "${tagname}"
  o printf '</h2>\n'
  add_filters
  if [[ ${tag} == top:* ]]; then
    o printf '<div id="filters"><a href="'
    o encode_url "/tags/${tag%:*}.html"
    if [[ ${tag} == top:*:* ]]; then
      o printf '">Show parent topic</a></div>'
    else
      o printf '">Show all topics</a></div>'
    fi
  fi

  o printf '<div id="content">\n'						# This is just to enable styling.
  call_hook hook_tagpage_before

  local oddity=odd								# Used for zebra stripes in CSS. (I know, new CSS versions can do that on their own.)
  local entry
  while IFS= read -r entry; do							# Reading each entry name from the current tag's array item.
										# Every line starts with the date, followed by a colon and the title of the entry.
    local entryname="${entry##*:}"						# Cut off the date/sortby string and the colon from the beginning.
    if [[ -z ${entryname} ]]; then continue; fi					# If the line is empty, stop right there.

    local tags
    tags="$(grab_tags "${shellbase}/entries/${entryname}")"||esub

    entrycount=$(( entrycount + 1 ))						# Counting how many entries have been processed so far for paging.

    local entrybasename="${entryname##*/}"

    local author
    author="$(grab_tag "${tags}" author)"
    local flags
    flags="$(grab_tag "${tags}" flags)"

    local title=

    local target=
    target="$(grab_tag "${tags}" redirect)"					# Check if there is an entry to which this entry should redirect.
    if [[ -n ${target} ]]; then							# If this is a redirecting entry.
      if option_set '>' && ! option_set '<'; then				# If the option to include redirects on tagpages is set and the option to ignore
										#   redirect tags is not set.

        local -i redirects_so_far=0

        while [[ -n ${target} ]]; do						# If there was a target defined
          local targetpath
          if targetpath="$(find "${shellbase}/entries/" \
            -name "${target}" -type f 2> /dev/null | grep .)"; then		# See whether a file of the name of the redirect target is in or somewhere below
        									#   the entries directory. If there is one (the target entry exists)
            o printf '<!-- The following is an entry redirection'
            o printf ' caused by the entry %s. -->' "${entryname}"
            entryname="${targetpath#$shellbase/entries/}"			# Switch this entry generating iteration to the redirect target entry, keeping...
            tags="$(grab_tags "${targetpath}")"||esub				# ... the already set output filename but getting the tags of the target entry.
            title="[Redirection to:] "
            target="$(grab_tag "${tags}" redirect)"				# Check if this is another redirecting entry.
          else
            w "The entry '${entryname}' should redirect to '${target}' but " \
              "this target does not exist. Including its original content " \
              "instead."
          fi
          ((redirects_so_far++))
          if ((redirects_so_far > max_entry_redirects)); then			# If there are more recursive redirects in a row than allowed.
            w "Too many redirections (at least ${redirects_so_far}). " \
              "Last entry was '${entryname}'."
            tags="$(grab_tags "${shellbase}/entries/${entryname}")"||esub	# If the redirect fails, fall back to the initial one, as if there would have been
            title=''								#   no redirect.
            break
          fi
        done
      fi									# /if option_set '>' or option_set '<'
      if ! option_set '<' && ! option_set '>'; then				# If neither the option to ignore redirect tags nor the option to include redirects
										#   on tagpages are set.
        continue								# Skip (don't include) this entry.
      fi
    fi										# /if taget var not empty
    title="${title}$(grab_tag "${tags}" title)"					# By adding the title at the end of the possibly empty string, the prefix that
										#   marks a redirection is kept.
    if [[ ${tag} == top* ]] || [[ ${tag} == *+top:* ]]; then			# Topic tagpages (combined or not) are displayed (and thus handled) differently.
      if [[ ${entry} == *" :::cut:::"* ]]; then					# So, here is this nifty and stupid trick again. the space before the delimiter ...
        o printf '<div class="stickied">'
        add_content "${shellbase}/entries/${entryname# }"			#   ... marks a stickied entry on a topic tagpage. tag names can't end in a space ...
        o printf '</div>'
        continue								#   ... those get stripped when the tags are grabbed, meaning this entry should ...
      fi									#   ... not be listed but instead it's content should be written to the outfile.
      local created
      created="$(grab_tag "${tags}" created)"
      local edited
      edited="$(grab_tag "${tags}" edited)"
      local sortby="${entry%:::cut:::*}"					# Get the sort criterium for this entry.
      										# $sortby here is the sort criterium of the entry - as it was saved in front of the
										#   entry name in the entry's line in the entrylists array - for the topic tagpage
										#   as well as the sub-heading (for sub-topics) on the sorted topic tagpage.
      if [[ ${sortby//:::cut:::/:} != "${tag}" ]] \
        && [[ ${previous_sortby} != "${sortby}" ]]; then			# If the current entry's sort criterium (being its complete tag name with
										#   parents) is not exactly this tagpage's tag and is different from the one
										#   of the previously processed entry.
        [[ ${sortby} == *":::cut:::"* ]] && {					# Only print heading if there would still be one after stripping the tag type.
          o printf '<h3 class="sub-top-heading"><a href="'			# Print a heading to mark the beginning of a new sub-topic.
          local sortby_cut="${sortby#*:::cut:::}"				# Cut off the tag type.
          o encode_url "/tags/top:${sortby_cut//:::cut:::/:}.html"
          o printf '">'
          o encode_html "${sortby_cut//:::cut:::/ - }"				# Print the tag replacing all ':::cut::' with ' - ' so it looks nice.
          o printf '</a></h3>\n'
        }
      fi
      local previous_sortby="${sortby}"						# Save the sort criterium for comparing it with the next entry's.

      if [[ ${oddity} == odd ]]; then oddity=even; else oddity=odd; fi		# Alternate between odd and even for CSS class.
      call_hook hook_tagpage_entry_top_before
      o printf '<div class="top-title-wrapper %s ' "${oddity}"			# This is just to enable styling.
      o encode_html_attr "$(printf '%s' "${tags}" \
        | grep --text --extended-regexp -- \
        '^cat:|^top:|^lang:|^author:|^note:|^flags:' | tr " " "-")"		# This is just to enable styling.
      o printf '">\n'								# This is just to enable styling.
      o printf '<div class="top-entry"><a href="'
      o encode_url "$(pathreducer "/entries/${entrybasename}.html")"
      o printf '">'
      o encode_html "${title:-(Entry Without Title)}"
      o printf '</a></div>\n'							# Add linked entry title
      o printf '<div class="entry-info"><a href="'
      o encode_url "$(pathreducer "/entries/${entrybasename}.html")"
      o printf '">'
      o encode_html "${created:+created on }${created:-Permalink }"		# Add created date or "Permalink" if no created date.
      o encode_html " ${edited:+ (last edited on $edited)}"			# Add edited date if one was set in the entry's header.
      o printf '</a>'
      if [[ -n ${author} ]]; then						# If this entry has an author tag.
        o printf ' by <a href="'
        o encode_url "$(pathreducer "/tags/author:${author}.html")"
        o printf '">'
        o encode_html "${author}"
        o printf '</a>'
      fi
      o printf '</div>\n'							# /entry-info
      call_hook hook_tagpage_entry_top_after
      o printf '</div>\n'							# /top-title-wrapper
    else									# If this is a tagpage of a tag type other than topic.
      o printf '<!-- next entry -->\n'
      o printf '<div class="entry-wrapper '					# This is just to enable styling.
      o encode_html_attr "$(printf '%s' "${tags}" \
        | grep --text --extended-regexp \
        '^cat:|^top:|^lang:|^author:|^note:|^flags:' | tr " " "-")"		# This is just to enable styling.
      o printf "\">\n"								# This is just to enable styling.
      local sortby
      sortby="$(grab_tag "${tags}" sort)"
      if [[ ${sortby} != about* ]] && [[ ${sortby} != stick* ]]; then		# If this is a stickied entry, don't print its title-wrapper.
        add_entry_chunk "${entryname}"
      else									# If this is a stickied entry.
        call_hook hook_tagpage_entry_stickied
      fi
      o printf "\n"
      above_entry_content							# Add reply, ref and re tags
      o printf "<div class=\"entry-content-wrapper\">\n"			# This is just to enable styling.
      call_hook hook_tagpage_entry_content_before
      add_content "${shellbase}/entries/${entryname}"
      call_hook hook_tagpage_entry_content_after
      o printf "</div>\n"							# /entry-content-wrapper
      below_entry_content
      o printf "</div>\n"
    fi

    if [[ ${tag} != top* ]] && [[ ${tag} != *+top:* ]]; then			# Tags of the type top: (and the tagage top.html) don't get page breaks.
      if (( lastpage > 0 )); then						# If there is more than 1 page to generate.
        if (( $(( (entrycount + 1) % perpage )) == 0 )) \
          || [[ ${lastentry} == $(( entrycount + 1 )) ]]; then			# If the next entry would be supposed to be on the next page or this is the last
										#   entry of this tag (last entries of single-page tagpages are excluded
										#   through below conditions anyway)
          o printf '<div class="pager">\n'					# This is just to enable styling.
          if (( pagecount > 0 )); then						# If this is not the first page
            if [[ ${tag} == all ]]; then					# If we are generating all.html instead of a single tagpage.
              o printf '<a href="'
              o encode_url "$(pathreducer "/${tag}.html")"			# Print link to first page
              o printf '">Go to first page</a> - <a href="'
              o encode_url "$(pathreducer "/${tag}-$(( pagecount - 1 )).html")"	# Print link to previous page
              o printf '">Go to previous page</a> - '
            else								# If we are generating a tagpage for an individual tag as opposed to all.html
              o printf '<a href="'
              o encode_url "$(pathreducer "/tags/${tag}.html")"			# Print link to first page
              o printf '">Go to first page</a> - <a href="'
              o encode_url "$(pathreducer "/tags/${tag}-$(( pagecount - 1)).html")"	# Print link to previous page
              o printf '">Go to previous page</a> - '
            fi
          fi
          o printf 'Page %d of %d' "${pagecount}" "${lastpage}"			# Print current page number and last page number.
          if (( pagecount < lastpage )); then					# If this is not the last page
            if [[ ${tag} == all ]]; then					# If we are generating all.html instead of a single tagpage.
              o printf ' - <a href="'
              o encode_url "$(pathreducer "/${tag}-$(( pagecount + 1)).html")"	# Print link to next page
              o printf '">Go to next page</a> - <a href="'
              o encode_url "$(pathreducer "/${tag}-${lastpage}.html")"		# Print link to last page
              o printf '">Go to last page</a>'
            else								# If we are generating a tagpage for an individual tag as opposed to all.html
              o printf ' - <a href="'
              o encode_url "$(pathreducer "/tags/${tag}-$(( pagecount + 1)).html")"	# Print link to next page
              o printf '">Go to next page</a> - <a href="'
              o encode_url "$(pathreducer "/tags/${tag}-${lastpage}.html")"	# Print link to last page
              o printf '">Go to last page</a>'
            fi
          fi
          o printf '</div>\n'							# /pager
          o printf '</div>\n'							# /content
          o printf '</main>\n'
          o printf '</section>\n'						# This was just to enable styling.
          add_footer
          if [[ ${lastentry} == $(( entrycount + 1 )) ]]; then break; fi	# End the tagpage here if there are no more entries to include.
          pagecount=$(( pagecount + 1 ))
          vv "   Starting page $pagecount."
          if [[ ${tag} == all* ]]; then
            outfile="${odir}/$(pathreducer "${tag}-${pagecount}.html")"		# Update the output filename so that every following entry will go to a new page.
          else
            outfile="${odir}/$(pathreducer "tags/${tag}-${pagecount}.html")"	# Update the output filename so that every following entry will go to a new page.
          fi
          if [[ -f ${outfile} ]]; then						# If the file already exists, check if it is writable ...
            [[ -w ${outfile} ]] || e "Can not write to" \
              "${outfile}. Permissions? Please check. Bug? Please" \
              "report. Thank you very much!"
          else									#   ... otherwise check if its directory is writable so the file can be created.
            [[ -w ${outfile%/*} ]] || e "Can not write" \
              "to ${outfile}. Permissions? Please check. Bug? Please" \
              "report. Thank you very much!"
          fi

          pagetitle="${tagname} (Page ${pagecount}) | ${sitename}"
          gen_head
          add_navbar
          o printf '<main id="tagpage">'					# This is just to enable styling.
          o printf '<h2 id="page-title">'
          o encode_html "${tagname} (Page ${pagecount})"
          o printf '</h2>'
          add_filters
          o printf '<div id="content">\n'					# This is just to enable styling.
        fi
      fi
    fi
  done <<< "${entrylists[${tag}]}"						# End of while IFS= loop.
  call_hook hook_tagpage_after
  o printf "</div>\n"								# /content
  o printf "</main>\n"
  o printf '</section>\n'							# This was just to enable styling.
  add_footer

  if [[ ${tag} == all ]]; then							# If we are generating all.html instead of a single tagpage.
    if (( lastpage > 0 )); then
      redirect "$(pathreducer all-0.html)" \
        "$(pathreducer all.html)"
    fi										# The "previous" link on page 1 links to ...-0.html instead of ....html.
  else
    if (( lastpage > 0 )); then
      redirect "$(pathreducer "tags/${tag}-0.html")" \
        "$(pathreducer "tags/${tag}.html")"
     fi										# The "previous" link on page 1 links to ...-0.html instead of ....html.
  fi

  call_hook hook_tagpage_end
}


# gen_tagpages():	CREATE TAGPAGES
#			  Generates first an array ($entrylists) with one array item per tag, then adds lines to these items: one line per entry, consisting of the
#			  entry's sort criteria and its name, then sorts these lists and then either generates
#			   - a single tagpage if one was desired with option -t or
#			   - all tagpages
# Globals used:		$desired_tagpage; $shellbase; $entrylists; $odir
# Vars modified:	$entrylists
# Inputs:		-
# Outputs:		stdout: messages; stderr: error messages
# Files written:	-
# Returns:		1 if desired tagpage does not exist/would be empty, otherwise 0
gen_tagpages() {
  vv "Preparing tagpage generation..."
  case "${desired_tagpage}" in							# This could be simplified, doesn't need to be a case statement anymore (todo). Tag
    all|top|'') : ;;								#   value filtering used to be here. Decided to move that into grab_tags() so it's
    cat:*|top:*|author:*|lang:*) : ;;						#   done earlier.
    *) e "The tag '${desired_tagpage}' is not of a supported tag type " \
         "(for generating a tagpage). Not generating it."
       ;;
  esac

  declare -A entrylists=()							# Array used to store entry names (one per line) for each tag.
  local entry
  for entry in "${shellbase}"/entries/**; do
    if [[ $(file --brief --mime "${entry}") != text* ]]; then continue; fi	# Skip this file if it's not a text file.
    entry="${entry#$shellbase/entries/}"					# Remove '$shellbase/entries/' from $entry - This leaves filename (with the rest
										#   of their path if they are in a sub-directory of entries/
    [[ -d ${shellbase}/entries/${entry} ]] && continue				# Don't use directories, only the files in them.
    [[ -f ${shellbase}/entries/${entry} ]] \
      || e "${shellbase}/entries/${entry}: no file there"
    local tags
    tags="$(grab_tags "${shellbase}/entries/${entry}")"||esub
    local created
    created="$(grab_tag "${tags}" created)"
    local author
    author="$(grab_tag "${tags}" author)"
    if [[ ${desired_author} ]] && [[ ${author} != "${desired_author}" ]]; then	# If option -a was used and this is not an entry by that author ...
      continue									#   ... Don't include this entry.
    fi
    local edited
    edited="$(grab_tag "${tags}" edited)"
    local sort
    sort="$(grab_tag "${tags}" sort)"
    if [[ -n ${sort} ]]; then
      local sortby="${sort}"
    else
      local sortby
      sortby="$(printf '%s\n%s' "${created:-1970-01-01}" "${edited}" \
        | sort --reverse | head --lines=1)"					# Pick the larger/later one of the two values, regardless of their formats ...
    fi
    call_hook hook_tagpages_entry
    local tag
    while IFS= read -r tag; do
      case "${tag}"
      in
        (top:*)									# If this is a topic tag.
          local parenttag="${tag}"
										# For topic tag pages, their tags instead of $sortby gets used for sorting.
          local stickied=""
          if [[ ${sortby} = about* ]] || [[ ${sortby} = stick* ]]; then		# OK, so this is a nifty and stupid trick. If there is a space after the tag and ...
            stickied=" "							#   ... before the last delimiter neither the tag name nor the entry name get ...
          fi									#   ... modified but the line gets sorted first and will be identifiable as stickied.
          entrylists["${tag}"]+=$'\n'"${tag//:/:::cut:::}${stickied}:::cut:::${entry}"	# Add a new line with that entry to the list.
          while [[ ${parenttag} == *:* ]]; do					# If the tag still contains a colon, it's a sub-tag.
            parenttag="${parenttag%:*}"						# Cut off the youngest child and ...
            entrylists[${parenttag}]+=$'\n'"${tag//:/:::cut:::}:::cut:::${entry}"	#   ... add a new line with this entry to the parent tag list as well.
										# This should also include "top" as a tag, leading in the generation of top.html
										#   when there is at least one topic tag in an entry.
          done
        ;;
        (author:*|cat:*|lang:*)							# If the tag starts with either of those, then add a new line with the
          entrylists["${tag}"]="${entrylists[${tag}]}"$'\n'"${sortby}:${entry}"	#  entry name to the array item corresponding to each tag of this entry.
        ;;&
        (author:*)
          if [[ ! ${desired_author} ]]; then					# Only include combined author tagpages if option -a (--author) was not used.
            local second_tags
            second_tags="$(printf '%s' "${tags}" \
              | grep --text --extended-regexp -- '^cat:|^top:|^lang:|^author:')"	# Get the other tags from this entry, but only these four types.
            local second_tag
            while IFS= read -r second_tag; do					# For each of them add to combined author tagpage to the list in the array so that
										#   tagpages filtered by author and one other tag at the same time are generated.
              [[ ${tag} != "${second_tag}" ]] \
                && entrylists["${tag}+${second_tag}"]+=$'\n'"${sortby}:${entry}"
            done <<< "${second_tags}"
          fi
        ;;
      esac
    done <<< "${tags}"
    if [[ ${sortby} =~ ^[0-9] ]] || [[ ${sort} =~ ^[0-9] ]]; then		# If the date starts with a number (i.e. is a date in the first place)
      entrylists[all]="${entrylists[all]}"$'\n'"${sortby}:${entry}"		# Add new line with entry name to "all", too.
    fi
  done
  mkdir --parents -- "${odir}/tags/" || e "tags directory not available."	# Create tags directory if it doesn't already exit.
  if option_set R; then								# If pathreducer mode in enabled.
    for file in "${shellbase}/tagicons/"*; do
      cp --update --no-preserve=all -- "${file}" \
        "${odir}/$(pathreducer "tags/${file##*/}")"				# Copy each tagicon individually, shortening the filename if necessary.
    done
  else										# If pathreducer mode is not enabled.
    cp --update --no-preserve=all -- "${shellbase}/tagicons/"* "${odir}/tags/"	# Copy all tagicons - if there are any - to the html directory.
  fi
  v "Finished preparing tagpage generation. ✅"
  call_hook hook_tagpages_start
  if [[ -n ${desired_tagpage} ]]; then						# If a tagpage name was given with option -t to generate a single tagpage.
    if [[ "${desired_tagpage}" != all ]] \
      && [[ "${desired_tagpage}" != top ]]; then				# If the desired tagpage is not one of 'top' or 'all'.
      if ! tag_exists "${desired_tagpage}"; then				# If the desired tagpage is not in the array of existing tags.
        e "The tag '${desired_tagpage}' does not seem to exist in any entry" \
          " of this web site."							# If desired tag doesn't exist: Ignore tag if force flag is set, abort otherwise.
          return 1
      fi
    fi
    if [[ ${desired_tagpage} == top* ]]; then
      entrylists[${desired_tagpage}]="$(printf '%s'\
        "${entrylists[${desired_tagpage}]}" | sort --key=1,1 \
        --field-separator=:)"							# Sort topic tagpages and 'top' (but not combined topic tagpage) this way.
    else
      if [[ ${desired_tagpage} == *+top:* ]]; then
        entrylists[${desired_tagpage}]="$(printf '%s'\
          "${entrylists[${desired_tagpage}]}" | sort --key=2,2 \
          --field-separator=:)"							# Sort combined topic tagpages after the colon belonging to 'top:'.
      else
        entrylists[${desired_tagpage}]="$(printf '%s' \
          "${entrylists[${desired_tagpage}]}"|sort --reverse)"			# Sort all other tagpages like this (differently from topic tagpages).
      fi
    fi
    gen_tagpage "${desired_tagpage}"
  else										# If no tagpage name was given with option -t to generate a single tagpage.
    local tag
    for tag in "${!entrylists[@]}"; do						# Cycle through all tags.
      if [[ ${tag} == top* ]]; then						# If this is 'top' or another topic tagpage (but not combined with another tag).
        entrylists[${tag}]="$(printf '%s' "${entrylists[${tag}]}"|sort \
          --key=1,1 --field-separator=:)"					# Sort the entrylist for that tag. If the entrylists array is outsourced into files
										#   in the future, "sed -i -f -" could be used to allow for extremely long tags.
      else
        if [[ ${tag} == *+top:* ]]; then					# If this is a combined topic tagpage...
          entrylists[${tag}]="$(printf '%s' "${entrylists[${tag}]}"|sort \
            --key=2,2 --field-separator=:)"					# ... then mind the extra colon of the second tag and sort according to the one
										#   belonging to 'top:'.
        else									# If this is not a topic tagpage at all.
          entrylists[${tag}]="$(printf '%s' "${entrylists[${tag}]}" \
            | sort --reverse)"							# Sort topic tagpages reverse chronologically (different from topic tagpages).
        fi
      fi
      p gen_tagpage "${tag}"							# Generate the tagpage for that tag.
    done
    wait_for_p									# Wait for any background processes from parallel generation to finish.
  fi
  call_hook hook_tagpages_end
  v "Finished generating tagpages. ✅"
  return 0
}


# gen_rss():		CREATE RSS FEED
#			  This generates a single RSS file that contains all entries that all.html contains.
#			  The entry's author name is used for the author tag. If none is provided, the site name is used. (This should be an e-mail address.)
# Globals used:		$desired_feed; $odir; $sitename; $url; $outfile; $desired_author; $entrylist
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: warning message
# Files written:	$outfile ($odir/all.rss)
# Returns:		-
gen_rss() {
  vv "Generating RSS feed..."
  [[ ${desired_feed} ]] \
    && w "Generation of (RSS) feeds for individual tags is not supported, " \
    "yet. I can only generate 'all.rss', which includes all entries."

  outfile="${odir}/all.rss"
  call_hook hook_rss_start
  {
    printf '<?xml version="1.0"?>\n'
    printf '<rss version="2.0" '
    printf 'xmlns:blogChannel="http://backend.userland.com/blogChannelModule">\n'
    printf '  <channel>\n'
    printf '    <title>'
    encode_xml "${sitename}"
    printf '</title>\n'
    printf '    <link>'
    encode_url "${url}"
    printf '</link>\n'
    printf '    <language>en-uk</language>\n'
    printf '    <description>All entries from %s (%s)</description>' \
      "${sitename}" "${url}"
    printf '    <lastBuildDate>'
    encode_xml "$(date --rfc-2822)"							# I know, there is hardly a reason to encode this. But if the date command
    printf '</lastBuildDate>\n'								#   ... is ever changed, I'd probbably forget to add th encode_xml.
    printf '    <docs>https://www.rssboard.org/rss-specification</docs>\n'
    printf '    <generator>SBWG %s</generator>\n' "${version}"
    printf '    <ttl>60</ttl>\n'
  } > "${outfile}"
  call_hook hook_rss_channel

  local entry
  gen_rss_this_entry() {								# Putting this block into a function so it can be called in the backround
											#  or not as needed. (For parallelising)
    entryname="${entry##*/}"
    local tags
    tags="$(grab_tags "${entry}")"||esub
    local author
    author="$(grab_tag "${tags}" author)"
    if [[ ${desired_author} ]] && [[ ${author} != "${desired_author}" ]]; then		# If option -a was used and this is not an entry by that author.
      return										# Don't include this entry in the feed.
    fi
    vv "- Generating RSS entry '${entryname}'."
    local title
    title="$(grab_tag "${tags}" title)"
    local pubdate
#    pubdate="$(grab_tag "${tags}" sort)"
#    test -z "${pubdate}" && pubdate="$(grab_tag "${tags}" created)"
    pubdate="$(grab_tag "${tags}" created)"
    test -z "${pubdate}" && pubdate="$(grab_tag "${tags}" edited)"
    local sort
    sort="$(grab_tag "${tags}" sort)"
    [[ "${sort}" =~ ^about ]] || [[ "${sort}" =~ ^stick ]] ||
      [[ "${pubdate}" =~ ^about ]] || [[ "${pubdate}" =~ ^stick ]] && return		# If it's a stickied entry, it doesn't belong in the feed.
    pubdate="$(date --rfc-2822 --date="${pubdate}")"
    o printf "    <item>\n"
    call_hook hook_rss_entry
    o printf "      <title>"
    o encode_xml "${title:-(ᵔᴥᵔ) Untitled Post}"
    o printf "</title>\n"
    o printf "      <link>"
    o encode_url "${url}$(pathreducer "entries/${entryname}".html)"
    o printf "</link>\n"
    o printf "      <author>"
    if [[ ${author} ]] && [[ ${emails["${author}"]} ]]; then				# If there is an author set for this entry and an email addr. for that author
      author="${author} <${emails["${author}"]}>"					#   then use that email address for the RSS author tag as per the standard.
    fi
    o encode_xml "${author:-$sitename}"
    o printf "</author>\n"
    o printf "      <description><![CDATA["
    add_content "${entry}"
    o printf "]]></description>\n"
    o printf "      <pubDate>"
    o encode_xml "${pubdate}"
    o printf "</pubDate>\n"
    o printf "      <guid>"
    o encode_url "${url}$(pathreducer "entries/${entryname}.html")"
    o printf "</guid>\n"
    o printf "      <category>"
    o encode_xml "$(printf '%s' "${tags}" | grep --text --extended-regexp -- \
      '^cat:|^top:|^lang:' | tr " " "-" | tr "\n" " ")"
    o printf "</category>\n"
    local atts
    atts="$(find "${shellbase}/entries/" \
    \( -name "${entryname##*/}.?*" -o -name "${entryname##*/}-?*" \) -type f -print)"
    if [[ -n ${atts} ]]; then
      while IFS= read -r att; do
        local attfulltype							# The file type of the attachement.
        attfulltype="$(file --brief --mime -- "${att}")"			# Mime type and encoding
        local attsubtype="${attfulltype%%; *}"					# Mime type incl. subtype
        local atttype="${attsubtype%%/*}"					# Just the mime type (no subtype)
        local attfn="${att##*/}"						# File name of the attachment
        vv "    Adding ${atttype} enclosure '${attfn}'."
        o printf '<enclosure url="'
        o encode_url "${url}$(pathreducer "/entries/${atttype}s/${attfn}")"
        o printf '" length="'
        local filesize
        filesize="$(du --bytes "${att}")"					# This gets the file size but with a tab and the file name after it.
        o encode_xml "${filesize%$'\t'*}"					# This cuts off the string from the tab onwards, leaving just the file size in bytes.
        o printf '" type="'
        o encode_xml "${attsubtype}"
        o printf '" />\n'
      done <<< "${atts}"
    fi
    o printf "    </item>\n"
  }

  for entry in "${entrylist[@]}"; do
    p gen_rss_this_entry							# Generate the entries.
  done
  wait_for_p									# Wait for any background processes from parallel generation to finish.
  o printf "  </channel>\n"
  call_hook hook_rss_end
  o printf "</rss>"
  v "Finished generating all.rss. ✅"
}


# print_usage():	Print usage information
# Globals used:		$scriptname
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: usage information
# Files written:	-
# Returns:		-
print_usage() {
  printf '%s\n' "Usage:
        ${scriptname} ACTION(S) [ OPTIONS ]

	${scriptname} [-c] [-b] [-g [GALLERY]] [-G GALLERYDIR]
          [-e [ENTRY] ] [-E ENTRYFILE] [-p [PAGE]] [-P PAGEFILE]
	  [-t [TAGPAGE] ] [-r] [-n] [-f] [-s [STYLE]]
	  [-i INPUT_DIR] [-o OUTPUT_DIR] [-n ENTRIES_PER_PAGE] [-R]
	  [-F] [-S SETTING] [-v[v]] [-d]

	${scriptname} -h [ HELP_PAGE ]

	${scriptname} -V"
}


# print_help():		Print a help page and exit
# Globals used:		-
# Vars modified:	-
# Inputs:		$1: help page name (optional)
# Outputs:		stdout: help
# Files written:	-
# Returns:		-
print_help() {
  case "${1}" in
    options|actions)								# In case help option argument is "options".
      more << END_OF_HELP
At least one option (action) is required to let the script know what it should
do. Multiple short (e.g. '-c -v -l logfile.txt') options can be combined (e.g.
'-cvl logfile.txt'). Arguments are passed to options separated by a space. Some
options require an argument after them, some have optional arguments. Long and
short options can be combined in one command. The following options are
available. There are additional help pages for many of them.

Actions:

	-c|--complete
		(Re-)generate the complete website. Further options to generate
		individual parts of the website are useless.

	-b|--blog
		(Re-)generate the blog, including entries, tagpages and rss
		feed.

	-g|--gallery|--galleries [GALLERY]
		(Re-)generate the gallery GALLERY. If GALLERY is not provided,
		(re-)generate all galleries.

	-G GALLERYDIR
		(Re-)generate the gallery that is located in the directory
		GALLERYDIR. GALLERYDIR can be located outsite of the web site's
		input directory.

	-e|--entry|--entries [ENTRY]
		(Re-)generate the entry ENTRY. If ENTRY is not provided,
		(re-)generate all entries.

	-E|--entryfile ENTRYFILE
		(Re-)generate the entry that is stored in the ENTRYFILE.
		ENTRYFILE can be located outsite of the web site's input
		directory.

	-p|--page|--pages [PAGE]
		(Re-)generate the page PAGE. If PAGE is not provided,
		(re-)generate all pages.

	-P|--pagefile PAGEFILE
		(Re-)generate the page that is stored in the PAGEFILE.
		PAGEFILE can be located outsite of the web site's input
		directory.

	-t|--tagpage [TAGPAGE]
		(Re-)generate the tagpage TAGPAGE. If TAGPAGE is not provided,
		(re-)generate all tagpages.

	-f|--files
		Copy the files/ direcrtory from the input directory to the
		output directory.

	-s|--style|--styles [STYLE]
		Copy stylesheet files that belong to the style STYLE from the
		input directory to the output directory. If (parts of) the site
		is generated as well, link stylesheet files belonging toSTYLE
		in the HTML headof every generated HTML file. If STYLE is not
		provided the default style as set in the site's settings file
		is used.

	-n|--navbar
		Generate the navigation bar and the navbar.html file. This
		option is automatically included if an action is performed that
		generates any HTML page or feed.

Verbosity options:

	-v|--verbose
		Be verbose. Writes interesting messages to stdout. Providing
		this option more than once (-vv) equals the option
		--very-verbose.

	-vv|--very-verbose
		Be very verbose. Writes many interesting messages to stdout.
		This option also sets the option -v (--verbose).

	-d|--debug
		Included messages intended for debugging purposes on stdout.
		This option also sets the options --verbose (-v) and
		--very-verbose (-vv).

	-l|--log [FILENAME]
		Log all messages to a file. Logging verbosity is equivalent to
		debug mode and independent of whether verbose mode, very
		verbose mode, debug mode, or none of them is enabled. If the
		filename is omitted, $logfile (default: sbwg.log in the

Other options:

	-a|--author AUTHOR
		Ignore all entries that do not have an author:AUTHOR tag in
		their header. (Re-)generates a site as if it didn't contain any
		entries by other authors/without an author tag.

	-i|--input INPUT_DIR
		Read the website source files from INPUT_DIR. If this option is
		not provided, the current working directory is used.

	-o|--output OUTPUT_DIR
		Write the generated website to OUTPUT_DIR. If this option is
		not provided, use the default output directory that is set
		using the setting 'odir' in the site's settings file.
		current directory) is used.

	-R|--reduce-paths [NUM]
		Shorten links/anchors, filenames and directory names to NUM
		bytes and remove any special characters except dashes,
		underscored and dots from them. If NUM contains two numbers
		separated by a dot (e.g. '8.3') the first value is used for
		the maximum basename length, the second one for maximum suffix
		length. If NUM is not given, the value of $maxfnl is used.
		Defaults to 255 bytes if no value is defined. Minimum value is
		'6' (6 bytes) or '6.0' (6 bytes, filename suffix removed).
		Note that the paths generated with this option will differ
		from those of a web site that was generated without this option
		and may therefore result in incompatible/dead links. This
		option slows down the generation process significantly.

	-S|--settings COMMANDS
		Source COMMANDS additionally and after the settings file.
		COMMANDS has/have to be supplied as a single word (e.g.
		encapsulated in quotes). If several commands are supplied, they
		need to be separated by a semicolon (';'). Variable assignments
		and function declarations supplied to this option override
		settings of the same name set in the settings file.

	-Q|--parallel
		This flag enables parallel mode. SBWG will try to process all
		entries, pages, tagpages, etc. at the same time in parallel.
		- This feature is experimental and has known bugs. It is
		recommended to only use it if you know what it is doing at
		this point.

	-C|--cache [CACHEGROUP[,CACHEGROUP2,...]]
		If this option is set, the script will create, keep and/or
		use cache files for parts that belong to the specified
		cache group. Currently the available cache groups are: tags,
		entries, navbar. If no cache group(s) is/are specified, cache
		files of all groups are kept and attempted to be used. If this
		option has been used before, using it again will make SBWG use
		the previously created persistant cache files and not generate
		those parts again unless cache files have been (re)moved
		manually.

	-U|--update-only
		(Nothing yet)

	-F|--force
		Continue processing and generating the web site even when an
		error occured, when the script suspects that there is a mistake
		in the options or input files or when it foresees that it will
		not finish without an error. Without this flag the script will
		abort when a file is missing or not accessible, when the output
		can not be written or some other error occurs. Use this option
		if the script wrongly skips input files because it suspects them
		to be of a wrong format or expects parts of them (tags) to cause
		problems down the line. You should check whether there really is
		no problem with the input files. Or in other words: Only use this
		option if you know what you are doing. If you have to use this
		option despite there being no problem with the input files, this
		can be considered a bug in the script and should be reported so
		that in can be avoided for other web sites.

	-V|--version
		Prints the version of SBWG and exits.

	-h|-?|--help
		Display this usage message on stdout and exit successfully.

END_OF_HELP
    ;;

    input)									# In case help option argument is "input".
      more << END_OF_HELP
The input directory is the root directory of the file structure that resembles
the source files of the website that should be generated into a browsable HTML
site. The input directory has to contain a 'settings' file in order to get
recognised a a SBWG input directory. All other files and directories and the
contents of the settings file are optional. All components of the site - if they
are included in the web site at hand - have to be placed in their respective
directory. For detailed information about the possible contents of a SBWG input
directory please refer to the README file or just peek in the example/ directory
to get an idea what goes where. The input directory may contain additional files
and directories that are not used by the script, are used by custom hooks or by
additional scripts.

Option: '--input INPUTDIR' or '-i INPUTDIR'
  Specifies the input directory - the directory that contains the source file
  structure of the web site. INPUTDIR can be a relative or absolute path to the
  input directory. If the option is omitted, SBWG will attempt to generate a web
  site from the file structure in the current working directory. The input
  directory has to contain a 'settings' file in order to be recognised
  by SBWG as a SBWG source directory. (See '--help settings')
END_OF_HELP
    ;;

    output)									# In case help option argument is "output".
      more << END_OF_HELP
The output directory is the directory where the final browsable HTML site will
be placed by the script. It has to be writable by the script. Existing files
will be overwritten if the script regenerates them. Additional files that should
be accessible publicly may be placed anywhere inside the output directory and
will not be touched by SBWG if SBWG doesn't generate files of the same name.
Please refer to the README file for more information on the output directory.

Option: '--output OUTPUTDIR' or '-o OUTPUTDIR'
  Specifies the output directory - the directory where the generated HTML site
  will be placed. OUTPUTDIR can be a relative or absolute path to the output
  directory.
  If the option is omitted, the output directory specified in the settings file
  will be used. If no output directory is specified there either, 'html/'
  inside the input directory will be used.
  Provided that you either want to use the default 'html/' or the setting 'odir'
  is set in the settings file, this option can be omitted when generating or
  updating a web site. But it is useful if you want to (re-)generate the web
  site into a directory different from the one you usually use, e.g. to test
  someting. For example you could create an alias for generating your web
  site into a directory where you can look at it before publishing it.
  Another example where this option is used is a multi-author blog of which one
  author also runs a personal blog with the same content. In that case the web
  site can be generated normally into one output directory and with the option
  '--author'/'-a' for a single-author version into a different directory.
END_OF_HELP
    ;;

    pages|page)									# In case help option argument is "pages".
      more << END_OF_HELP
A SBWG page resembles a single static HTML page. SBWG will generate an HTML file
with header, navigation bar and footer from the source file. The source file of
a SBWG page consists of an optional 'title' tag at the top of the file
(see '--help tags') and the content of the page below after that. The content is
a block of HTML code that will be included unaltered in the resulting HTML page
by SBWG. See the README file for more information on SBWG pages.

Option: '--page [PAGENAME]' or '-p [PAGENAME]'
  Generates/updates all SBWG pages from the 'pages/' directory. If PAGE is
  specified: Genertes/updates only the specified page."
  This option can be used to update the web site when the content(s) of only
  (a) page(s) have been changed. If nothing else has been changed, this is the
  quickest way to update the web site.

Option: '-P PAGEFILE'
  While option '--page'/'-p' can be given the name of a single page that should
  be generated or updated, option '-P' takes the filename of a SBWG page that
  should be generated. This can be a file in the pages/ directory of the web
  site or any other file. This means that even files that are not part of the
  source file structure of the web site can be integrated using this option.
  As of right now, only one path/filename can be passed to this option.
END_OF_HELP
    ;;

    blog|blogs)									# In case help option argument is "blog".
      more << END_OF_HELP
A SBWG web site can have one weblog with as many entries as the generating
computer can handle. The page all.html contains all entries ordered by
'sort' tag > 'edited' date > 'created' date (split into several pages if there
are many entires). Additionally a tagpage for each category, topic, language and
author tag that is used in at least one entry is created. Furthermore a combined
tagpage for each tag that any author has used at least once is created, on which
only entries with that tag from that author are included. That means SBWG can be
used to create multiple blogs from different authors as long as all authors trust
each other.

The blog consists of the entries, tagpages and the RSS feed. Those three elements
can be created individually (see '--help options'). They are also created in a
complete web site generation (option '--complete' or '-c'). There is also an
option for only generating/updating the parts of the web site that make up the
blog in one option:

Option: '--blog' or '-b'
  Alias for '-e -t -r'. Generates/updates all parts of the weblog of a web site:
  entries, tagpages and the RSS feed. When a new blog entry has been added or
  existing entries or their tags have been edited and nothing else has been
  changed, only this option is necessary to get a complete update of the web
  site. If nothing else has been changed since the last update, this will lead
  to the same result as a complete re-build with option '--complete'/'-c'.
  If you deliberately don't want to (re-)generate the RSS feed, you can use
  both options '--tagpage'/'-t' and '--entries'/'-e' to generate/update the
  complete weblog without the RSS file.Generating RSS en
END_OF_HELP
    ;;

    entries|entry)								# In case help option argument is "entries".
      more << END_OF_HELP
An entry in SBWG is a piece of content that can contain zero, one or more tags
(like a title, categories, topics, author name and other meta data) and a text
or HTML part. All entries together make up the webblog of the web site. You can
think of an entry as a single blog post or blog entry. Entries are located at
the directory 'entries/' in the web site's source directory (input directory).
Each file in that directory resembles one entry.

Option: '--entry [ENTRYNAME]' or '-e [ENTRYNAME]'
  Generates/updates all entries from the 'entries/' directory but not the
  corrosponding tagpages. If ENTRYNAME is specified: Generates/updates only the
  specified entry.
  You will probably not find this optionvery useful on its own as it will
  generate/update the HTML files of entries themselves but not the tagpages of
  a weblog, where the content of the entries is also included. This option is
  part of the option '--blog'/'-b', which (re-)generates the entrie weblog.

Option: '-E ENTRYFILE'
  While option '--entries'/'-e' can be given the name of a single entry that
  should be generated or updated, option '-E' takes the filename of a SBWG entry
  that should be generated. This can be a file in the entries/ directory of the
  web site or any other file. This means that even files that are not part of
  the source file structure of the web site can be integrated using this option.
  As of right now, only one path/filename can be passed to this option.
END_OF_HELP
    ;;

    attachment|attachments|att|atts)						# In case help option argument is "attachments".
      more << END_OF_HELP
Blog entries can have file attachments. Those files will be linked for download
below the entry content. Images and audio files will be embedded. Text files can
not be used as attachments at the moment. A file gets attached to an entry when
it is somewhere in the entries directory and named after the entry. That is, the
attachment file's name is either the entry name followed by a file extension or
the entry name followed by a dash and anything after that. If the entry name
is 'foo', any non-text file named foo-something, foo-anything.ext or foo.ext
will be attached to that entry.

File attachments are copied to the output directory and thumbnails of image
attachments are created when the entry they belong to is generated. This means
that the command line options '--entries'/'-e' and '-E' create the required
attachment files in addition to embedding/linking them in the generated HTML
document, if the generated entry/entries has/have attachments to their name.
END_OF_HELP
    ;;

    comments|comment|email|emails)						# In case help option argument is "comments".
      more << END_OF_HELP
It is possible to have a comment link generated below entries of a specific
author by setting the email address of this author in the settings file. If
there is an email address set for an author, every entry that has this author
name defined with an author: tag in its tag header will include such a comment
link. The comment link is just a mailto link with the email subject and first
line of the body pre-filled.

To set an author's email address, assign it to the array element in the
$emails array that corrosponds to the author name.
Examples:
emails[Foo]=foo@example.com # Sets the email address for the author 'Foo'.
emails['James Barrie']=james-blog-comments@example.com # Sets the email address
for comments on entries tagged 'author:James Barrie'.

If an email address is set for an author, this address will be accessible
publicly on every page that includes any entry of that author and in the RSS
feed.
END_OF_HELP
    ;;

    tagpages|tagpage)								# In case help option argument is "tagpages".
      more << END_OF_HELP
For every category, topic, language and author tag that exists in any entry in
the weblog, an HTML page is generated that includes or lists all entries that
are tagged with that tag. For topic tagpages, only the titles, authors and
dates are listed and entries are grouped by topic and sub-topic. For all other
tags the tagpages include the entire entries (blog view) and entries are sorted
by the creation date ('created:' tag), edited date ('edited:' tag) and sort
value ('sort:' tag). A tagpage is also created for every author+tag combination.
With these combined tagpages it is possible to view entries tagged with a
certain category, topic or language and filter them to show only entries by one
author.

Option: '--tagpage [TAGNAME]' or '-t [TAGNAME]'
  Generates/updates tagpages (topic, category, language and author tagpages as
  well as combined tagpages) for all entries but not the entries themselves. If
  TAGNAME is specified: Genertes/updates only the tagpage for that tag.
  As this option only generates the pages listing the blog content for a
  specific tag but not the entry pages, you will likely not find this option on
  its own very useful. It is part of the option '--blog'/'-b' that is
  recommended to be used for generating/updating a weblog.
END_OF_HELP
    ;;

    feeds|feed|rss)								# In case help option argument is "feeds".
      more << END_OF_HELP
Currently, only one RSS feed is created for the entire weblog. No other formats
and no individual feeds for single tags. Both may change in future version. The
RSS feed is very basic, doesn't usually validate and contains the contents of
all blog entries unparsed and unfiltered.

Option: '--rss' or '-r'
  Generates/updates the RSS feed all.rss.
  If this option is specified, the file 'all.rss' in the output directory (the
  generated web site's root) will be (re-)generated, containing all entries of
  the web site. This can be considered a rudimentary solution to the requirement
  that readers of the weblog can subscribe to it with a feedreader, nothing
  more. Other feed formats and RSS files for individual tagpages are not
  supported, yet.
END_OF_HELP
    ;;

    galleries|gallery)								# In case help option argument is "galleries".
      more << END_OF_HELP
A SBWG image gallery simply consists of a directory containing at least on image
file. An image gallery can be linked to an entry by having the same name.
The image gallery feature is still very basic. It is not high up on the list of
planned extensions and improvements. Currently only JPEG files are supported,
sorting is not possible, nested galleries aren't possible yet and the styling of
the galleries is also very basic out of the box. No Javascript, no shadow boxes
are used. The size of generated thumbnails and preview images can be changed in
the settings file.

Option: '--gallery [GALLERYNAME]' or '-g [GALLERYNAME]'
  Generates/updates the gallery pages. If GALLERYNAME is specified: Generate
  only the specified gallery.
  If this option is specified without an argument, all image files from all
  galleries in the web site's source directory will be copied to the output
  directory, any non-existing thumbnails and other sizes will be generated
  and all HTML files for all gelleries will be (re-)generared. Thumb nails
  and other image sizes generated by SBWG will not be updated if the files
  already exist.
  If the option is used with an argument (e.g. '-g my-gallery'), the same
  tasks are performed but only for that one gallery (my-gallery).
END_OF_HELP
#Option: '-G GALLERYDIR'
#  With this option, you can generate a gallery by giving this option the name
#  of the directory that holds the gallery. The differencd to option
#  '--gallery'/'-g' is that this can also be a path that is outside of the web
#  site's source directory.
    ;;

    files)									# In case help option argument is "files".
      more << END_OF_HELP
Option: '--files' or '-f'
  Copies the files from the files/ directory to the root of the output
  directory.

  If the option is specified, all files from the files/ directory inside the
  input directory are blindly copied to the files/ directory in the output
  directory. This means that any files with the same name that already exist
  in the output directory's files/ directory will be overwritten if possible
  but files that exist in the output directory but not in the input
  directory's files/ directory will not be touched.

  If pathreducer mode is enabled (option '--reduce-paths'/'-R', see '--help
  pathreducer') the paths of files in the files/ directory including their
  filenames will be modified and shortened if necessary to meet the rules
  of the pathreducer. Note that this means that links to files, whether from
  inside the web site or from external web sites, will have do be adjusted
  accordingly. It is best to make sure that directory names and filenames
  inside the files/ directory do not contain any characters that could be
  removed or modified by the pathreducer in order to prevent surprises or
  dead links. If you want to use pathreducer mode but not for copying the
  files/ directory, you can run the script once with option '--reduce-paths'/
  '-R' and without option '--files'/'-f' and a second time with option
  '--files'/'-f' but without the option '--reduce-paths'/'-R'.
END_OF_HELP
    ;;

    pathreducer|reduce-paths)							# In case help option argument is "pathreducer".
      more << END_OF_HELP
Option: '--reduce-paths [MAXFNL[.MAXSL]]' or '-R [MAXFNL[.MAXSL]]'
  If this option is supplied, all file names generated by the script (that
  includes entry files, page files, tagpages, arbitrary files from the files/
  directory as well as links in generated HTML files) will be reduced to
  contain only lowercase letters, numbers, underscores, dashes and dots and
  will be no longer than MAXFNL bytes.

  MAXFNL (the maximum filename length) can be set as an argument to this
  option, as the variable $maxfnl in the settings file or it can be omitted
  to use the default of 255 bytes.

  MAXFNL can be an integer to mean the maximum length of directory names and
  filenames or it can be two integers separated by a dot ('.') to define both
  the maximum filename length without filename extension/suffix and the
  maximum length of the suffix. An example of this two-value usage is '8.3'.
  This will only create directory names and filenames that abide by the
  Short File Name restrictions of old FAT filesystems of MS DOS.

  Using pathreducer mode slows down the generation process significantly. It
  should only be used if directory names or file names that can not be written
  to the filesystem of the output diectory are expected, e.g. because of very
  long tags or because the site will be hosted on an old DOS machine.

  Using this option can lead to directory names and filenames that differ from
  the original names (of course, that's the point of this option). That means
  that links to files (HTML files, image files, stylesheets, downloads, ...)
  can change when this option is used, not used or used with a different value
  for $maxfnl from previously. It should therefore be a conscious decision to
  either use or not use this option every for a public web site.
END_OF_HELP
    ;;

    styles|style)								# In case help option argument is "styles".
      more << END_OF_HELP
Option: '--style [STYLESETNAME]' or '-s [STYLESETNAME]'
  Copy/update the CSS files for the style set specified in the settings file to
  the output directory. If STYLESETNAME is specified: Copy/update the files
  belonging to that style set to the output directory and use that style in the
  header of HTML files if any HTML files are generated in this run of the
  script.
  Without this option, or the '--complete'/'-c' option, which includes the
  '--styles'/'-s' option, SBWG will not copy any style sets from the source
  directory to the output directory. Since the CSS files usually don't get
  changed as frequently as the content of a web site, you have to specify this
  option whenever you want to update the CSS in the output dirtectory. In the
  headers of the generated HTML files every file belonging to the style set
  specified in the web site's settings file will be linked. If no style set is
  specified in the settings file, the default set named 'stinpell' will be
  used.
  If the option '--styles'/'-s' is specified without an argument, the same
  style set is used and all files belonging to it are copied to the output
  directory.
  If the option is used with an argument, e.g. '-s my-style', all files
  belonging to the style set with that name (my-style) are copied and linked
  in the header of every generated HTML file.
END_OF_HELP
    ;;

    settings)									# In case help option argument is "settings".
      more << END_OF_HELP
The 'settings' file must exist in the web site's source directory in order for
SBWG to recognise the directory as a SBWG web site source directory. SBWG will
refuse to generate the website if this file isn't there and it will assume that
there is a valid SBWG web site source structure in the same directory if the
file exists and contains a SBWG version comment as its first line. The first
line of the 'settings' file has to start with '#SBWG VERSION', where VERSION
is the minimum version number of SBWG the settings file and web site are
compatible with. This is just to prevent confusion and unnoticed mistakes or
missing features in web sites that have been generated with an incompatible
version of SBWG. Example for the first line of a SBWG settings file:

#SBWG 0.9.11

You can use the file to inject any Bash code that should be sourced before the
web site is generated. Commonly it is only used to set a few global variables
and declare hooks. You can copy the settings file from the example directory or
peek at it for explanations of the settings that should be set for any new web
site. The settings file can be empty if you don't want to declare any hooks and
only want to use default settings. But I suggest that you include at least the
'sitename' setting and, if the web site contains a blog, the 'url' setting.
There are more settings worth knowing about. Have a look at the settings file in
the example directory that comes with this package.

Settings and hooks can also be added or overridden via an argument to the
command line option '--settings'/'-S'.
END_OF_HELP
    ;;

    hooks)									# In case help option argument is "hooks".
      more << END_OF_HELP
A hook is a function that is declared in the settings file of a web site. (See
'--help settings'.) It does not exist in the SBWG script and therefore is not
executed if it is not included in the settings file. No hook is required. There
are many hooks that get executed at different points in the generation process
of a web site. Therefore they can be used to inject code at certain points of
the script for only the web site who's settings file contains the hook. This way
the script does not need to be edited for customisation and different web sites
can be customised differently.
Inside a hook all variables that exist at the point at which the hook is
executed can be read and changed. Output can be added to the file that is being
generated at that point, additional commands can be executed, and so on. This
makes the settings file a very flexible tool for the customisation of a web site
and SBWG itself, and a possible source of errors when not used carefully. When
writing an elaborate new hook (as opposed to copying an example hook or hook
from an existing settings file) you will probably not be able to avoid reading
at least some of the SBWG script to see how your goal can be accomplished.
There are a couple of helper functions in the script that of course can also be
used in hooks. I will not explain here every part of the code that could
possibly be of relevance when writing a new hook. I don't know what you want to
do with your hook, so I wouldn't know what to limit myself to. There is a table
that explains all hooks in the README file that comes with this package.
END_OF_HELP
    ;;

    parallel)									# In case help option argument is "parallel".
      more << END_OF_HELP
Option: '--parallel [NUM]' or '-Q [NUM]'
  If this flag is set, the script will try to process some parts of the web
  site generation in parallel.

  Currently this feature is in a very rudimentary and highly experimental state.
  The method of paralleling the generation process will completely change
  when/if this feature is completed. It is not recommended to use this option.
  Expect this option to make trouble in its current state.
END_OF_HELP
    ;;

    force)									# In case help option argument is "force".
      more << END_OF_HELP
Option: '--force' or '-F'
  By default (if the flag '--force' or '-F' is not set) the script will
  suddenly stop when it suspects something to be wrong, leading to an
  incomplete or not fully updated web site. If the force flag is set, the
  script will try to continue despite errors. This may also lead to an
  incomplete, but more likely to a broken or otherwise unusable web site in the
  output directory. Examples where the force flag takes effect: When generation
  of a single entry, page or tagpage is desired (arguments to options '-e',
  '-E', '-p', '-P' or '-t') but the desired file or tag does not exist or is
  otherwise not usable. When the partition where the temporary files are stored
  or of the output directory is full. When a tag is too long to be used as a
  filename. In any other situation where SBWG detects an error and wants to
  abort. It is not recommended to use this option. It is pretty much only useful
  to circumvent undiscovered bugs that abort the script for no good reason.

  Use this option only if you know why you need to use it and expect things to
  go wrong.
END_OF_HELP
    ;;

    debug|debugging)								# In case help option argument is "debug".
      more << END_OF_HELP
The --debug option (short version: -d) is meant for myself to debug the script.
I use it to display additional output while adding new features. Most but not
all of this output is removed in the published versions of SBWG. You can use it
if you want the maximum amount of output when using the script. But it's likely
that you won't find it useful. Note that more output the script generates, the
longer the generation proceess takes to finish.
END_OF_HELP
    ;;

    tag|tags|tagtype|tagstypes)							# In case help option argument is "tag" or "tags" or "tagtype" or "tagtypes".
      more << END_OF_HELP
Tags are meta-data that is assigned to SBWG entries and pages in their source
file headers. The use of tags is always optional. They should be used when
creating a weblog, though. Without any tags there is no structure to the blog
entries. One tag can be defined per line. Each tagline has the format:

    tagtype:tagvalue

There are different types of tags that are used differently by SBWG. Some
tagtypes allow for subtags. For those tagtypes multiple colons (:) can occur
in one line:

    tagtype:tagvalue:subtagvalue:subsubtagvalue(:...)

For SBWG pages, only the title tag or no tag is allowed. For entries the
following types of tags exist:

title		Title of the entry or page
created		Creation date or other string used for sorting
edited		Last edited date or other string used for sorting. Overrides
		'created'.
sort		Bogus date or other string to override 'edited' and 'created'.
author		Name of the author of the entry
cat		Category
top		Topic (and possibly sub-topic)
lang		Language of the entry
re		Marks the entry as connected/being an answer to another entry.
ref		Marks this entry as referring to another entry.
reply		Marks this entry as a reply to another entry.
updateof	Marks this entry as up update of another entry and the other
		entry as an older version of this entry.
redirect	Replaces the content with the content of another entry/redirects
		to another entry.
flags		String of flags, separated by any or no character.
note		Include a note above the entry content.
#		A line starting with a '#' (hash symbol) is a comment. The line
		is ignored by SBWG.

For much more information on tags and how to use them, please see the Tags
section in the README file.
END_OF_HELP
    ;;

    flag|flags)									# In case help option argument is "flags".
      more << END_OF_HELP
A flag is an option that can be enabled for individual entry. A flag is set by
including its name in the value string of the 'flags:' tag. Only one flags tag
per entry is allowed. Multiple flags can be set by adding them to the flags
tag. They can be separated, e.g. by spaces, colons or commas, or not.

The following flags exist:

nocomment	Will exclude the comment/email link from the entry regardless
		of whether an email address is set for the author of the entry.
hideatts	Do not include links or embeddings of file attachments in the
		generated HTML.
hideimageatts	Like `hideatts` but only affecting image files.
hideaudioatts	Like `hideatts` but only affecting audio files.
hidevideoatts	Like `hideatts` but only affecting video files.
hideotheratts	Like `hideatts` but only affecting files of mime types other
		than image, audio and video, which only leaves application
		files because text files aren't supported as file attachments.
(custom flags)	Any string of characters may be included in the flags string
		without having an effect on the entry of its own. Custom flags
		can be processed in hooks and CSS.

The following flags are planned for future versions of SBWG:

noheader	Will exclude the header/title wrapper (the title and other tag
		information) of the entry when it is generated into tagpages.
noshow/hide	Will exclude the entire entry when tagpages are generated.
sticky		Will sort the entry at the beginning of tagpages regardless of
		any 'created', 'edited' or 'sort' tags.
END_OF_HELP
    ;;

    messages|verbosity|logging|log)						# In case help option argument is "messages" or "verbosity" or "logging".
      more << END_OF_HELP
There are four levels of verbosity to stdout: none, verbose, very verbose and
debugging. The corrosponding options are: No additional option for no output
except warnings and errors, option --verbose or -v for verbose mode, option
--very-verbose or -vv for very verbose mode and option --debug or -d for
debugging mode. The latter is mainly meant for myself and will likely add no
helpful output for the user.
Additionally there is a logging option: --log or -l for creating a log
file that will contain all messages (as in debug mode) independently of the
chosen verbosity to stdout. If no argument is passed to option --log/-l, a file
by the name specified in the global variable $logfile will be used. By default
this will be sbwg.log in the current directory, but this can be changed in a web
site's settings file. If an argument is added after the option --log/-l it will
be used as path/filename for the log file.
END_OF_HELP
    ;;
    *)										# In case no or an unexpected argument for the help option has been provided.
      printf 'Generates a web site from text files in a SBWG source directory.\n\n'
      print_usage
      printf '\nMore help with:

	--help|-h|-? HELP_PAGE

Available HELP_PAGEs:
        attachments, blog, comments, entries, feeds, files, galleries, hooks, input, options, output, pages, pathreducer, settings, styles, tagpages, tagtypes\n'
    ;;
  esac
  exit
}



# set_cachefile():	Set the cache file according to the caching option(s) for an item that is about to be generated.
# Globals used:		$cachefile
# Vars modified:	$cachefile
# Inputs:		$1: type of cache file; $2: filename
# Outputs:		stderr: warning messsges
# Files written:	$cachefile
# Returns:		-
set_cachefile() {
  (( $# == 2 )) || (( $# == 1 )) \
    || e 'set_cachefile() received wrong number of arguments.'
  local dir
  [[ ${desired_caches} = *"${1}"* ]] && dir="${shellbase}" || dir="${tmpdir}"	# Persisting dir if caching of this part is desired, temporary cache dir otherwise.
  cachefile="${dir}/$(pathreducer "cache/${1}${2:+/${2}}")"			# Set the cachefile that will be used by the c() function from now on. If $2 has
										#   been given, preceed it with a slash.
  test -d "${cachefile%/*}" \
    || mkdir --parents -- "${cachefile%/*}" \
    || w "Cache directory not available."					# If the directory the chachefile will be in does not exist, try to create it.
}



# set_logfile():	Set the log file./Change where log output will be redirected to.
# Globals used:		$logfile
# Vars modified:	$logfile
# Inputs:		$1: log file
# Outputs:		stderr: warning messsges
# Files written:	$logfile
# Returns:		-
set_logfile() {
  logfile="$(readlink -f "${1}")"						# Set logfile to absolute path.
  [[ -w ${logfile} ]] || :|install -D /dev/stdin "${logfile}" \
    || {
         w "Log file '${logfile}' can not be created."
         logfile="$(readlink -f SBWG.log)"
       }									# Make sure the log file and all directories above it exist.
  [[ -d ${logfile} ]] && {
                           w "'${logfile}' can't be used as a log file. " \
                             "It's a directory."
                           logfile="$(readlink -f SBWG.log)"
                         }
  exec 2> >(tee --append "${logfile}" 1>&2)					# Redirect stderr to both the logfile and stderr so that errors from subprocesses
										#   won't be missed.
}


# option_set():		Check if a certain option is set.
# Globals used:		$options
# Vars modified:	-
# Inputs:		$1: letter(s) of the short option
# Outputs:		-
# Files written:	-
# Returns:		0 if option is set, 1 if not
option_set() {
  [[ ${options} == *${1}* ]]                                                    # Check whether the provided option letter ($1) is in the string
}										#   which would mean it was set by set_option.


# option_set_multi():	Check if a certain option is set more than once.
# Globals used:		$options
# Vars modified:	-
# Inputs:		$1: letter(s) of the short option
# Outputs:		-
# Files written:	-
# Returns:		0 if option is set more than once, 1 if not
option_set_multi() {
  [[ ${options} == *${1}*${1}* ]]                                               # Returns 0 if the provided character appears more than once in $options.
}


# check_options():	Check for conflicting options and stuff.
# Globals used:		$options
# Vars modified:	-
# Inputs:		-
# Outputs:		stdout: messages; stderr: error message
# Files written:	-
# Returns:		-
check_options() {
  d "options: ${options}"
  if ! printf '%s' "${options}" \
    | grep --quiet -- "e\|E\|p\|P\|t\|r\|f\|s\|g\|G\|n"; then			# If none of these options are set then don't continue because there would be
										#   no point and it would be confusing to get no output, no error, exit code 0
										#   but also nothing done.
    e "None of the used options do anything on their own. " \
      "Maybe see --help."
  fi
  if printf '%s' "${options}" \
    | grep --quiet -- "e\|E\|p\|P\|t\|r\|g\|G" \
    && ! option_set n; then set_option n;  fi					# If an option to generate HTML is set but the navbar would not be generated, do ...
										#   ... generate the navbar because this includes necessary preparations.
  if option_set_multi e; then v "More than one option to generate entries by name is set. I'll only use the last one that has an argument."; fi
  if option_set_multi E; then v "More than one option to generate entries by path is set. I'll only use the last one that has an argument."; fi
  if option_set_multi p; then v "More than one option to generate pages by name is set. I'll only use the last one that has an argument."; fi
  if option_set_multi P; then v "More than one option to generate pages by path is set. I'll only use the last one that has an argument."; fi
  if option_set_multi t; then v "More than one option to generate tagpages is set. I'll only use the last one that has an argument."; fi
  if option_set_multi s; then v "More than one style option is set. I'll only use the last one that has an argument."; fi
  if option_set_multi i; then v "More than one option for the input directory is set. I'll only use the last one that has an argument."; fi
  if option_set_multi o; then v "More than one option for the output directory is set. I'll only use the last one that has an argument."; fi
  if option_set_multi a; then v "More than one author has been defined. I'll only use the last one that has an argument."; fi
  if option_set_multi h; then v "I can only display one help topic at a time. '${scriptname} --help' to get all the help."; fi

}


# set_option():		Adds one or more option letter(s) to the $options string so that we can check later what options are set/whether a certain option is set.
# Globals used:		$options; $logfile
# Vars modified:	$options; $desired_gallery; $desired_entry_name; $desired_entry_file; $desired_page_name; $desired_page_file; $desired_tagpage;
#			  $desired_feed; $style; $maxfnl; $shellbase; $odir; $desired_author
# Inputs:		$1: Option letter(s) that will be set; $2: Argument for the option represented by the last character of $1 (optional)
# Outputs:		stdout: messages; stderr: warning messages, error messages
# Files written:	-
# Returns:		-
# Adds an option letter to the string so that we can check later what options are set/if a certain option is set.
# Also sets a global variable if the passed argument is one that belongs to the option that is being set.
set_option() {
  options="$(printf '%s%s' "${options}" "${1}")"				# Add option letter(s) to the options string so we can easily check later
  										#   whether it has been set.
  # shellcheck disable=SC2015
  case "${1: -1}" in								# Look at the last of the option letters because that one may have an argument.
    b) set_option etrn ;;
    c) set_option petrsfgn ;;
    d) set_option vv ;;
    g) [[ ! "${2}" =~ (^-|^$) ]] && desired_gallery="${2}" ;;
    G) [[ ! "${2}" =~ (^-|^$) ]] && desired_gallery_path="${2}" ;;
    e) [[ ! "${2}" =~ (^-|^$) ]] && desired_entry_name="${2}" ;;
    E) [[ ! "${2}" =~ (^-|^$) ]] && desired_entry_file="${2}" ;;
    p) [[ ! "${2}" =~ (^-|^$) ]] && desired_page_name="${2}" ;;
    P) [[ ! "${2}" =~ (^-|^$) ]] && desired_page_file="${2}" ;;
    t) [[ ! "${2}" =~ (^-|^$) ]] && desired_tagpage="${2}" ;;
    r) [[ ! "${2}" =~ (^-|^$) ]] && desired_feed="${2}" ;;			# This is not actually tested, yet. Argument is not actually used. Please fix.
    s) [[ ! "${2}" =~ (^-|^$) ]] && style="${2:-elth}" ;;			# Do nothing if no argument for option -s is provided. This ensures that the option
										#   will not change the style sheet set to be used from the default to being empty.
    R) [[ ! "${2}" =~ (^-|^$) ]] && maxfnl="${2}" ;;	        		# If argument for option -R/--reduce-paths is provided, set the max.
                                                                                #   filename length accordingly, overriding the default and the settings file.
    S) [[ ! "${2}" =~ (^-|^$) ]] && eval "${2}" \
         || e  "Option -S or --settings needs a good argument." ;;		# Source the argument for option -S/--settings as if it was in the settings file.
    l) [[ ! "${2}" =~ (^-|^$) ]] && logfile="$(readlink -f "${2:-SBWG.log}")" ;;
    i) [[ ! "${2}" =~ (^-|^$) ]] && shellbase="$(readlink -f "${2}")" \
         || e "Option -i or --input needs a good argument." ;;
    o) [[ ! "${2}" =~ (^-|^$) ]] && odir="$(readlink -f "${2}")" \
         || e "Option -o or --output needs an argument." ;;
    a) [[ ! "${2}" =~ (^-|^$) ]] && desired_author="${2}" \
         || e "Option -a or --author needs an argument." ;;
    C) [[ ! "${2}" =~ (^-|^$) ]] && desired_caches="${desired_caches},${2}" ;;
    h) print_help "${2}" ;;
    v|d|f|F|m|n|Q|U|'>'|'<'|w) ;;						# These options just need their letters set and not throw an error, nothing else.
    _|')') ;;									# Dummy option "letters". Enables using set_option just to judge the following
										#   argument without executing anything option specific.
    ?) e "Unfamiliar option '-${1}' - see '${scriptname} --help'" ;;		# If a single option letter is getting set that is not known, throw an error. ...
										#   ... Note that this means that any longer string that contains one or more ...
										#   ... or exclusively unknown option letters will not result in an error.
  esac
  d "Arg for set_option '${1}': '${2}'"
  [[ ! "${2}" =~ (^-|^$) ]]							# Return 0 if the next argument belongs to the first one.
										#   This enables the possibility to use this function to check whether there is an
										#   argument provided for an option. (Whether the next argument exists but does
										#   not start with "-".)
}



###############################################################################################
# Fuctions end here, script starts here (apart from variables that have been set at the top). #
###############################################################################################

tmpdir="$(mktemp --directory --suffix=.SBWG)"					# Generate a path were we will store temporary files during this run of the script.
if [[ ${tmpdir: -1} == / ]]; then tmpdir="${tmpdir::-1}"; fi			# Remove trailing slash if there was one so we can be sure what we have.
readonly tmpdir

(( ${#} == 0 )) && e "I don't know what to do. Please --help!"			# Don't do anything if no argument/option is provided.

declare -i n=0
declare -i i
for arg										# Use this counter instead of shift because the arguments will be parsed properly
do										#   a second time below. This here is just to get four options (-i, -h, -V and -l)
  n=$(( n+1 ))									#   done before the other options are determined/checked.
  case ${arg} in								# The -i/--input command line option has to be found first, if it is present.
    -i|--input) i=$(( n+1 )) ;;							# $i will be used after this loop to set $shellbase.
    -h|-\?|--help) h=$(( n+1 ))
                   [[ ! "${!h}" =~ (^-|^$) ]] && h="${!h}" || h=		# Assign h argument only if it wouldn't start with a '-' (i.e. isn't another option).
                   print_help "${h}"						# While we're at it, also check for the help option, which should work and print the
                   exit								#   help screen independently and regardless of other or absence of optionns.
    ;;
    -V|--version) printf '%s\n' "${version}"; exit;;				# Oh, and the --version/-V option too.
    -l|--log|--logging|--logfile|--log-file) l=$(( n+1 )); set_option l "${!l}"	# Option l has to be set early on so that no output is missing in the log file.
    ;;
  esac
done
shellbase="$(readlink -f "${!i:-$(pwd)}")"					# If no shellbase has been set yet, set it to the current working directory.
										# This has to be set first. Otherwise the settings file with settings that are to
										# be set before the command line options are set can not be found.
option_set l && set_logfile "${logfile}"
readonly logfile

call_hook hook_start
while (( ${#} > 0 ))
do
  case "${1}" in
    -+([A-Za-z])*) declare -i i							# If this is a short option (starting with a single dash).
                   for (( i=1; i<((${#1}-1)); i++ )); do			# Cycle through each letter of the word from the second to one before the last.
                     set_option "${1:$i:1}"					# Set the option of this letter.
                   done
                   set_option "${1: -1}" "${2}" && shift ;;			# Set the last of the options as well but pass the argument to it is one is given.
    --complete) set_option cpetrsfgn ;;						# A complete re-build consists of generating galleries, blog and pages and copying
										#   files and styles.
    --blog|--blogs) set_option etrn ;;
    --gallery|--galleries) set_option ng "${2}" && shift ;;			# Set option g, skip next argument if it's an argument belonging to -g
    --gallerydir) set_option nG "${2}" && shift ;;				# Set option G, skip next argument if it's an argument belonging to -G
    --entry|--entries) set_option ne "${2}" && shift ;;				# Set option e, skip next argument if it's an argument belonging to -e
    --entryfile) set_option nE "${2}" && shift ;;				# Set option E, skip next argument if it's an argument belonging to -E
    --page|--pages) set_option np "${2}" && shift ;;				# Set option p, skip next argument if it's an argument belonging to -p
    --pagefile) set_option nP "${2}" && shift ;;				# Set option P, skip next argument if it's an argument belonging to -P
    --tagpage|--tagpages) set_option nt "${2}" && shift ;;			# Set option t, skip next argument if it's an argument belonging to -t
    --rss) set_option nr "${2}" && shift ;;					# Set option r, skip next argument if it's an argument belonging to -r
    --files) set_option f ;;							# Set option f for copying the files directory.
    --style|--styles) set_option s "${2}" && shift ;;				# Set option s, skip next argument if it's an argument belonging to -s
    --input) set_option i "${2}" && shift ;;					# This is left in because it handles all eventual cases nicely even though ...
										#   ... option i is actually checked and set before the loop now.
    --output) set_option o "${2}" && shift ;;					# Set option o, skip next argument if it's an argument belonging to -o
    --navbar) set_option n ;;							# Set option n for generating the navbar.html file.
    --author) set_option a "${2}" && shift ;;					# Set option a, skip next argument if it's an argument belonging to -a
    --magic) set_option m; v "Mixing in some magic color. 🌈" ;;		# There is no comment in this line.
    --verbose) set_option v ;;							# Set option v for verbose mode.
    --very-verbose|--veryverbose) set_option vv ;;				# Set option v for very verbose mode.
    --log|--logging|--logfile) set_option "(l)" "${2}" && shift ;;		# Option has been dealt with previously. But the conditional shift is still needed.
    --debug) set_option dvv ;;							# Set debug mode, which includes very verbose mode.
    --waiting-animation) set_option w ;;
    --reduce-paths) set_option R "${2}" && shift ;;				# Set option R, skip next argument if filename length is provided.
    --set|--setting|--settings) set_option S "${2}" && shift ;;			# Set option S, skip next argument if settings were provided.
    --parallel) set_option Q; v "Experimental parallel mode is enabled." ;;	# Enable parallel mode (experiemntal).
    --force) set_option F; v "Force mode is enabled. Will ignore " \
      "errors if any occur." ;;							# Set force mode: Force continuation when an error or mistake is suspected.
    --cache) set_option C "${2}" && shift; v "Caching mode is enabled. " \
      "Will keep/use some/all persistant caching files for future use." ;;	# Enable caching: Create cache files in the input directory for faster processing.
    --update-only|--update) set_option U; v "Update-only mode is " \
      "enabled. Will only generate new content and ignore existing files " \
      "that may have changed. EXCEPT THAT THIS IS NOT ACTUALLY IMPLEMENTED YET SO THE PREVIOUS SENTENCE IS NOT TRUE" ;;
#      "that may have changed." ;;						# Set update-only mode: Only generate things that are new (ignore changes in
										#   previously existing files).
    -*) e 5 "What a weird unfamiliar option '${1}' - see '${scriptname} --help'" ;;
    *) e 5 "Unwanted argument '${1}' - see '${scriptname} --help'" ;;
  esac
  shift
done

check_options									# Check for colliding and redundant options.

if ! [[ -f ${shellbase}/settings ]]; then					# If there is no settings file
  e 5 "I can't see a settings file in \"${shellbase}\". Are you sure " \
    "this is where the source files of the website you want to generate " \
    "are? If the path is correct and you don't want to set any setting, " \
    "run the following command to create an empty settings file for this " \
    "version of SBWG: echo '#SBWG ${version}' > '${shellbase}/settings'"
else										# If there is a settings file in directory where the website source resides
  if ! IFS= read -r -n 6 she < "${shellbase}/settings"; then			# If the first few characters of the file can't be read
    e 5 "Settings file can't be read."
  else										# If the first few characters of the file were read
    if [[ ${she} != "#SBWG " ]]; then						# If it doesn't seem to be a SBWG settings file
      e 5 "'${shellbase}/settings' doesn't appear to be a SBWG settings " \
        "file. Make the first line '#SBWG ${version}' or a lower version " \
        "number if it is meant for this version of SBWG."
    else      									# If it seems to be a SBWG settings file
      read -r setver < "${shellbase}/settings"					# Read the first line of the settings file
      if [[ $(printf '%s\n%s' "${setver##* }" "${version}" \
        | sort --sort=version --reverse | head -n1) != "${version}" ]]; then	# If the script version is older than what the settings file demands
        e 5 "The version of SBWG is too old. The web site's settings file " \
          "says it will only work with SBWG versions ${setver##* } and above."
      else									# If the script is not too old
        source "${shellbase}/settings"						# Overwrite variables that are set at the top of this script file and source hooks.
      fi
    fi
  fi
fi

d "logfile: ${logfile}"								# Can't have these lines sooner because option d would not have been set, yet.
										#   log file gets changed by the settings file.

if option_set_multi d; then							# If debug mode is enabled ...
  declare -a command_history=()
  trap 'debug_trap' DEBUG							#   ... call this function for every command that is executed.
fi


[[ -z ${odir} ]] && odir="$(readlink -f "${shellbase}/html")"			# Set default output directory if none has been set.

#check_options									# Check for colliding and redundant options.

date="$(date --iso-8601)"

option_set v && { waiting_ani_stop; waiting_ani_start; }			# This line is here to make sure that the waiting animation is started if so far no
										#   v/vv/d has been called and that only one is running if v/vv/d had been called.

prepare
option_set s && copy_styles							# Option -s or --style or --styles
option_set f && copy_files							# Option -f or --files
option_set r && gen_rss								# Option -r or --rss
option_set p || option_set P && gen_pages					# Option -p or --page or --pages or -P
option_set e || option_set E && gen_entries					# Option -e or --entry or --entries or -E
option_set t && gen_tagpages							# Option -t or --tagpage or --tagpages
option_set g && gen_galleries							# Option -g or --gallery or --galleries

call_hook hook_end
v "I'm done now. ✅"
clean_up 0
e "Script ended unexpededly-ish."



################################################################################################################################################################
#
# Conventions
#
# * No output is produced unless something seems or goes wrong (warning or error) or one of the options -v -vv or -d is used.
# * v	is used to print the rough progress of the script to let the user know through stdout which part of the generation process is undergoing or finished.
# * vv	is used to let the user knwo through stdout which item the script is currently processing as well as note finding which don't require action or examination.
# * d	is used to print all sorts of debugging output that a user usually does not need to see or know.
# * w	is used to print a warning whenever the script suspects something to be wrong or faulty. The script will continue with the things it can do.
# * e	is used to print an error message and exit the script/abort unless force mode (option --force or -f) is enabled.
# * Custom hooks should note that they have been called and what they are doing at least with a debug message (function d()).
#
################################################################################################################################################################


################################################################################################################################################################
#
# File Descriptors (Handles)
#
# 0	stdin		Not really used, actually.
# 1	stdout		Output meant for the terminal. Verbosity depending on options.
# 2	stderr		Error output from external processes and from error messages that warrant the script exiting/aborting.
# 3	(reserved)	For output that is meant for the log file. Highest verbosity (includes all messages including debug messages)
# 4	(reserved)	c?
# 5	(reserved)	v?
# 6	(reserved)	vv?
# 7	(reserved)	d?
# 8	(reserved)	?
# 9			Used for processing stuff here and there (e.g. in loops).
#
################################################################################################################################################################


################################################################################################################################################################
#
# Function overview
#
# Helper functions
# o()				Runs a command and redirects/appends its output to the file that's currently being generated (path stored in $outfile)
# c()				Similar to o() but additionally saves the output in a cache file at a location in the $cachedir resembling the path stored in
#				  $outfile.
# v()				Prints a string to stdout if verbose mode, very verbose mode or debug mode is on.
# vv()				Prints a string to stdout if very verbose mode or debug mode is on.
# d()				Prints a string to stdout if debug mode is on.
# w()				Prints a warning message and aborts after clean-up if force mode (option -F/--force) is not set.
# e()				Prints an error message and aborts the script after clean-up.
# option_set()			Returns true if the passed letter is among the currently set script options.
# option_set_multi()		Returns true if the passed letter is at least twice among the currently set script options.
# set_option()			Adds an option letter to the script options. (Same as command line option letters.) Is meant to be used only at start of script.
# set_cachefile()		Set the cache file according to the caching option(s) for an item that is about to be generated.
# set_logfile()			Set the log file./Change where log output will be redirected to.
# redirect()			Creates a hard link if possible, otherwise creates an HTML file that only redirects to another URL.
# gen_head()			Generates the HTML head for the current $outfile using $pagetitle as the page title.
# add_navbar()			Appends the previously generated navbar file to the current $outfile.
# add_content()			Appends the content part of a source file to the current $outfile.
# add_footer()			Appends the footer file's content to the current $outfile.
# call_hook()			Checks whether a function exists and if so calls it. Used for optional hooks in the sourced settings file.
# is_number()			Checks whether the passed argument is a simple integer or not.
# tag_exists()			Checks whether the passed string is an item in the $tagslist array (whether the tag exists)
# grab_tag			Outputs the first occurance of a tag in an entry or page source file.
# grab_tags			Outputs the header (consisting of tag lines) of an entry or page source file.
# pathreducer()			If wanted and necessary, converts each element of a path to be no more than $maxfnl bytes long and removes special characters.
# encode_html()			Encodes chacaters that are problematic in HTML.
# encode_xml()			Encodes chacaters that are problematic in XML/RSS feeds.
# encode_attr()			Encodes chacaters that are problematic in HTML attributes for various reasons.
# encode_class()		For classes and IDs, whitespaces should be replaced with something else so that e.g. category values with spaces won't be interpreted
#				  as several classes.
# encode_url()			Percent-encodes chacaters that are problematic (unsafe or could possibly be reserved in some scheme) in URIs.
# decode_url()			Decodes percent-encoded characters.
# trim()			Trims any and all leading and trailing whitespaces from the string passed as $1 or read from stdin.
# has_flag()			Checks whether a string is contained in the flags variable.
# waiting_ani_start()		Hides the cursor and starts the waiting animation unless one is already running (does not currently work if parallel mode is enabled.
# waiting_ani_stop()		Stops the waiting animation if one is currently running.
# show_waiting_ani		Draws the waiting animation at the current cursor position. Don't use this in hooks, etc. Use waiting_ani_start() instead.
#
# Functions that generate a part of the web site
# prepare()			Miscellaneous preparations. Is assumed to run once before any content generation is done.
# gen_navbar()			Generates the navbar files in the temporary directory. The file is included in every HTML file that is generated.
# copy_files()			Copies the files from the input directory to the output directory.
# copy_styles()			Copied the files belonging to the style set $style to the output directory.
# gen_entries()			Prepared/initiates generation of entries. Calls gen_entry on every entry that should be generated in this run.
# gen_entry_page()		(Formerly gen_entry()) Generates the HTML file for one entry.
# add_entry_chunk()		Generates and adds the title-wrapper part of an entry. (It's the same an entry pages and tagpages).
# gen_gelleries()		Prepared/initiates generation of gelleries. Calls gen_gallery on every gallery that should be generated in this run.
# gen_gallery()			Generates the HTML file and various image sizes for one gallery.
# gen_pages()			Prepared/initiates generation of pages. Calls gen_page on every page that should be generated in this run.
# gen_page()			Generates the HTML file for one page.
# gen_tagpages()		Prepared/initiates generation of tagpages. Calls gen_tagpage on every tagpage that should be generated in this run.
# gen_tagpage			Generates the HTML file(s) for one tagpage.
# gen_rss()			Generates the RSS feed (all.rss file).
#
################################################################################################################################################################


################################################################################################################################################################
#
# Variable overview
#
# Global variables:	Description:											Usually set in:
# $version		The version string/version number of this script.						script file
# $sitename		The name/title of the web site as it may appear in the title bar or page header.		settings file (fallback in script)
# $url			The domain name under which the web site can be found - used for links in RSS and ATOM feeds.	settings file (fallback in script)
# $style		The name of the style set/set of CSS files that will be used.					settings file (fallback in script)
# $perpage		The number of entries that will be listed per page on tagpages with more than $perpage entries.	settings file (fallback in script)
# $thumbsize		Size into which thumbnails will be resized (format MAXWIDTHxMAXHEIGHT)				settings file (fallback in script)
# $minisize		(Formerly $thumbsizemini) Size into which minis will be resized (format MAXWIDTHxMAXHEIGHT)	settings file (fallback in script)
# $previewsize		Size into which previews of gallery images will be resized (format MAXWIDTHxMAXHEIGHT)		settings file (fallback in script)
# $smallsize            Size into which previews of entry pictures will be resized (format MAXWIDTHxMAXHEIGHT)          settings file (fallback in script)
# $cutoff		Variable not supported/used. Was supposed to be used for the maximum length of entry previews.	-
# $tagiconsize		Size into which tagicons will be resized (format MAXWIDTHxMAXHEIGHT)				settings file (fallback in script)
# $headinsert		Depricated. Was used to insert code in HTML header of a web site. Usage of hook recommended.	-
# $scriptname		Just the name of the script, for reference on help pages, error messages, etc.			script file (constant)
# $tmpdir		Path of the temporary directory that is used during and removed after this run of the script.	script file (constant)
# $pagetitle		Line of single page, set before HTML head is generated. Defaults to $sitename after each usage.	settings file (but changed for each page)
# $emails		Associatative array of email addresses for authors of weblog entries.				settings file
# $entrylist		Array/list of all entries that have been found in the site's source directory.			gen_navbar()
# $gallerylist		Array/list of all image galleries that have been found in the site's source directory.		gen_navbar()
# $tagslist_unsorted	Array/list of all tags that have been found in all entries, before deduplication and sorting	gen_navbar()
# $tagslist		Array/list of all tags that have been found in all entries, sorted, no duplicates		gen_navbar()
# $entrylists		Associative array with lists of all entries that have a certain tag - key is the tag		gen_tagpages()
# $options		String of all option letters that are set in this run of the script.				set_option() (used through command line)
# $shellbase		Path of the web site's source directory. Base path of all source files. 			script file
# $odir			Path of the output directory - The directory where the HTML web site is written to.		set_option(), settings file, script fallback
# $cachedir		Path of the caching directory used in the current run of the script.				set_option(), settings file, script fallback
# $logfile		Path/filename of the log file, in case logging is enabled.					script file
# $maxfnl		The maximum allowed/desired filename length without filename extension in bytes.		set_option(), settings file, script fallback
# $maxprocs		The maximum number of sub-processes that are allowed when generating in parallel mode.		set_option(), settings file, script fallback
# $max_entry_redirects  Maximum number of recursive redirections from 'redirect:' tags that are allowed.		script file
# $updateslist		Associative array that stores entry names for which a newer version exists. For cross-linking.	script file
# $waiting_ani_frames	Array of strings, each resembling a frame in the waiting animation				settings file, fallback in script file
# $waiting_ani_speed	Speed of the waiting animation; number in Hertz							settings file, fallback in script file
# $waiting_ani_delay	Number of seconds the script waits after terminal output before the waiting animation starts	settings file, fallback in script file
# $max_debug_commands   Maximum number of recent commands that should be remembered in debug mode.			script file
#
# Commonly used local variables:
# $outfile		Path and name of the file that's currently being processed. Used by many functions, mainly through o().
# $cachefile		Path and name of the file that's being used to store the cache of the currently prcessed item.
# $tags			This variable is commonly used to store a newline separated list of all taglines of the currently processed entry/page. It is always local
#			  but sometimes expected to be filled before certain functions are called.
#
################################################################################################################################################################


################################################################################################################################################################
#
# Option characters (option letters)
#
# v	Verbose mode (two or more: very verbose mode) is enabled
# d	Debug mode (two or more: more debug outout) is enabled
# w	Waiting animation is enabled
# e	Generate entries
# g	Generate galleries
# h	Show help and exit
# i	Input directory set via command line option
# l	Logfile set via command line option
# m	Magic colors
# n	Generate navbar
# o	Output directory set via command line option
# p	Generate pages
# r	Generate RSS feed
# s	Style set chosen via command line option
# t	Generate tagpages
# C	Persistant caching mode
# E	Generate an entry file
# F	Force mode is enabled
# P	Generate a page file
# Q	Parallel mode is enabled
# R	Pathreducer mode is enabled
# S	Settings complemented by command line
# U	Update only mode is enabled
# >	Include redirects on tagpages
# <	Ignore redirect tags
#
################################################################################################################################################################


################################################################################################################################################################
#
# Error codes (exit codes)
#
# (none)	Not specified. There might be no error.
# 0		No error. Script ended as expected. There may have been warnings or not.
# 1		Generic error. Script ended with an error of an unspecified type and origin.
# 5		Error during early preparations. Script ended with an error before basic checks were finished.
# 7		Directory locked. Script ended because either the input directory or the output directory has a lock file.
#
################################################################################################################################################################


################################################################################################################################################################
#todo
#
# next:
# - bug: if option -V is supplied as part of multiple stringed together options, it's not recognised (eg. sbwg -vV) - also check whether -i -h and -l have the same problem
# - entry with invalid date appears in thunderbird (through rss) as from today. fix sorting in rss. handle invalid dates in rss.
#     - should rss use the sort tag after all?
#     - document fixed/forced date format if necessary (right now i think the readme only suggests one)
# - when sbwg is called with no action or with no argument at all, a warning is created, but the only warning is the one about there having been at least one warning
# - bug?: if number of taglines of an entry have changed after a persistant cache file has been created, the content will be wrong. to solve, create cache files with content as well? or include content in chunks?
# - bugs in parallel mode:
#     - no tags in navbar
#     - HTML tags sometimes don't get closed
# - bug?: if gallery images are read-only in the input directory, they will be read-only in the output directory, so they can't be generated again
#     - what about style files and files files and file attachments?
# - cachefile removal of sbwg-editentry doesn't work
# - bring more sense in the permanent caching situation again:
#     - (let) automatically remove caches of files that have been touched after the last time the site was generated
#     - new cache type/group: list of entries for a given tag
#     - what should be the default in future versions?
#         - Option -C (--cache) on and automatically discarding caches of since then changed files? only if it works really well in every possible/usual circumstance!
#         - Option -U (--update-only): this would be independent from caching and off by default, right? only generate files that don't exist in the odir yet.
#
#
# (new) features:
# - generate sitemap
# - embedded video player for video attachments
# - when switching to atom and/or rss2: keep generating the old all.rss file, but with only one item: a message with the current date as title and info about the new feed in the description
# - generate an rss feed for each tagpage
#     - allow --rss/-r to take argument for desried feed
# - new tag type 'menu:' for pages and entries
# - a tag type or file nameing convention for entries that should be ignored as of now (drafts)
#     - better: a setting, which tags should be excluded from the generation (e.g. cat:draft)
# - option improvements: check for multiple letters after a dash, use equal sign for long version options
#     - for the equal sign: add "starting with '--optionname=' to the case check
#     - for the multiple short options combined: check for starting with '-i' instead of is '-i'
#         - if something remains after that, turn that into an option with a dash so it can be identified by the next loop?
#         - or: if argument starts with dash, loop through all its letters
#         - either way: recognise the last letter and interpret the following argument if there is one and it doesn't start with a dash as an argument to that last option letter
# - create install script
# - maybe change functions to have the desired_... variable set the list of items to be generated to only that one, before these things are tested
#     - support multiple arguments for desired_tagpage, desired_entry, etc. this should also enable using "subdirectory/*" as an argument (when subdorectories are supported in the future).
# - list entries with no top: tag under top:none or something similar? Or make a page that lists all entries that don't have any tags that enable them to be found? (or just that don't have any cat:, top: or redirect: tags?
# - create cat.html that lists all existing category tags in such a way that it can be made a tag cloud with css
# - stickied entries for topic tagpages
# - generate atom feeds
# - create galleries.html or galleries/index.html that lists all galleries
#     - just an idea: icons for each gallery for: number of images, corrosponding entry exists
#     - link to corrosponding entries as well
# - video support in galleries
# - Add a FAQ file
# - flag ideas: noshow/hide, noheader, nogal, nouse/ignore, sticky, nopage, hideatts, noatts, collapsed/spoiler
# - new tag type: book: (or call it something else) for entries (and/or pages?) that represent chapters, kind of like top: but with a more book-like index and sorted solely by chapter. this could be done manually by using menu: though
# - setting in settings file for combined tagpages: which combinations should be generated? default: combos=( author )
# - new option for switching to interactive mode, where other option don't matter and the user is asked questions enter paths and names interactively/is lead to making all decisions step by step
# - maybe: put many many parts of the HTML generation in separate functions so that those functions can be overwritten or disabled.
# - create tag cloud for categories (and for topics?)
# - support galleries in subdirectories
#     - group galleries in navbar according to directory
#         - have them collapsed by default
#     - create html page per directory with list of galleries in that directory
#     - add option -G for $desired_gallery_dir (or _path?)
# - idea for future restructuring of content handling:
#     - only one content directory named 'content'
#     - subdirectories content/pages/, content/entries/, content/books/ (and galleries?)
#     - variables (or array?) for default input directories of the different content types
#     - tag type 'type:' to declare a source file to be of that type
#         - if a file has the wrong type, ignore it
#         - if a file has no type and is in the right directory, use it
#     - tag type 'block:' or 'place:' or something for placing a piece of content in the navbar top, navbar bottom, above content, in the footer, in the header, etc.
# - new option that does nothing but call a hook (hook_custom or something similar)
#     - it would take an argument and pass that to the hook
#     - document this hook in README, help pages, usage information, howto
# - maybe: error_exit if sourcing the settings file produces errors/an error
# - nicer tag display: heading for category, topics, etc., then exclude the tagtypes
#
# particularities:
# - change gen_page, gen_entry, etc. to take the full file/dir path as argument - options -P, -E and -G should be able to be used with page/entry/gallery not inside the input dir
# - use coreutil's timeout for convert and other possibly intensive parts of sbwg? if so, use global variable to set delay.
# - use of css should be optional
# - new option ('w'?) for enabling the waiting animation
# - galleries link in navbar gets placed there even when there is no galleries directory
# - weblog link (all.html) gets placed in navbar even when there is no entry.
#     - what about all.html itself? that shouldn't be started if it's going to be empty.
# - introduce long command line options for options that have no command line options yet (--ignore-redirects, --redirects-on-tagpages)
# - function to remove an option from the options string? (use case: SBWG is called with some options by default/aliased but the author of a web site's settings file wants to make sure certain options are not set)
# - support BMP and GIF files (and other types?) in galleries? (JPEG2000, HEIC, TIFF, and so on)
# - make sure no web log related things are generated if there are no entires. a web site without a web log should be possible.
# - allow to use force to ignore SBWG version line in settings file?
# - change gals back to galleries! and change all documentation accordingly.
# - include hook_header and hook_footer or whatever they are called in the example web site's settings file by default to look for header and footer files and include them if they exist so the user has the option to expand the hook or use files.
# - attachments: change check for mime type to check for subtype, too, so that application/ogg can be treated as video
# - add flags as classes for css, make sure spaces stay in
# - decide: when should persistent caches be created? when should they be used? both by default (off only with option?)
# - combined tagpages for tags that only one author has used are generated - they shouldn't be generated - the filter is not included on these tags anyway, as this would not make any sense
# - use checkboxes for several things to enable css tricks
#     - like: file attachments, entire navbar, navbar headings, entry content bodies
# - use all files of a style set name, not just css; or at least use css and js files, but better all files because images may be needed for a styles, e.g. for background images
# - update FAQ about file and directory permissions
# - always use single quotes where double quotes aren't necessary
# - better display of tags in entry title wrappers: add heading ('Categories', 'Topics', etc., remove tagtype ('cat:', 'top:', etc.)
# - create helper script to check for problems with tags in source files (unallowed characters, multiple taglines of tag types that don't allow more than one occurance, etc.)
# - use path for $style so that style sets can be used from sbwg directory or from custom site source directory or anywhere really? absolute paths and paths reöative to input dir should be allowed, sbwgstyles dir will be used if the style does not exist as a relative path
# - add hooks: before generating starts, in add_navbar, in other helper functions, in help printing function (enable adding own help pages?)
# - create helper function "isimage" or something to check the format of a file to decide whether to proceed - this could reduce redundancy a little bit
# - also copy files files if size is different?
# - only output gallery generation information in very-verbose mode
# - add option -G for generating gallery by path
# - create tagpage for entries not tagged with any author
#     - link that page under "by author" in the navbar
#
#
# nishnash:
# - check that all entities that can ever be passed as a command line option argument (entries, tagpages, pages, (...?)) are handled/looped through with their full filename and make sure that globs can be used as command line option arguments
# - change where css files are taken from: first web site directory, then global sbwg directory - web site directory css should always come first
# - note tags should be acceptable in page headers, not just entries
# - comments (#) in source file headers are used double in cache files because/if they have no colon in them. they should be ignored completely.
# - for future tests of all sorts of things with all sorts of content and styles and files and options and arguments
#     - test all options with example site, draft0 and trap site
#         - script that runs them all in a row - maybe option -S to modify sitename for easier checking of whether everything is as expected
#     - different LANG, LC_ALL and LC_CTYPE
#     - recursive redirecting - or at least redirecting to another redirection
#     - run on slower computer
#     - run with newer bash verisons
#     - many different and weird filename suffixes, with pathreducer
#     - find functions that i can pass weird stuff to and pass weird stuff to them
#     - call functions from settings file and pass them stupid shit
# - maybe: go through all of the script and think of lots of small options that could possibly be neet and create global variables for them - like `tags` dir in output, alternative names for gals and other dirs in input dir, flags like log without date, ...................
# - make pathreducer better:
#     - use iconv to convert to similar characters - ex.: iconv -f utf8 -t ascii//TRANSLIT < /tmp/utf8_input.txt > /tmp/ascii_output.txt
#     - support maxfnl=ext/ext2/ext3/ext4/fat/fat16, etc
#     - adapt pathreducing to what the filesystem supports (uppercase, special characters, filename length, suffixes)
#     - support maxfnl=auto
#         - detect filesystem with mount or something
#             - check for each directory/part?
#         - that way pathreducer could be enabled by default and turned off with '--reduce-paths=off'
# - RSS:
#    - exclude stickied posts from feeds
#    - exclude redirections from feeds? or rather redirect them
#    - append thumbnails of related pictures to rss items
#    - convert/properly escape content/description in rss feed (html escape stuff for spaces, <, >, ...)
#    - create an rss file for each tagpage
#    - maybe introduce a variable to define the maximum amount of entries in the all.rss feed. paging/pagination can be implemented in the atom feed. rss is for making compromises https://datatracker.ietf.org/doc/html/rfc5005#section-3
# - convert images in parallel if parallel is available. or what about xargs?
# - option --fix or --fix-entry or something for re-generating one entry and all tagpages and feeds it's on?
#
#
# Will not do for now (for good and for bad reasons) and tasks with low priority:
# - use --mime-type instead of --mime for file checks?
# - allow .entry and .txt filename suffixes (suffix will be excluded in entry names/urls but having it in the filename allows for filename extension based type recognission, e.g. in windows.)
# - find a replacement for the perl script to filter out broken unicode characters and control characters in tag values
# - if the target of a redirect tag has no title, use its name
# - create helper functions that check whether a file exist, is a regular files, is writable, ... and error_exits with a message if not
# - when changes are made to sbwg that require changes in web site: create update script?
# - so, slashes in certain tagtypes are currently replaced with dashes because they would be mistaken for path element separators. would it help in any way, or be better, to replace them with newline characters instead because newline characters definitely aren't part of a tag value. or what about some harmless control character? they could be replaced back to slashes inside pathreducer or something.
# - problem: if --debug is not the first option, some debug output is missing (also, log file change by option still creates two logfiles)
# - case insensitivity for tagtypes
# - case insensitivity for tag values?
# - improve image gallery generator
#    - test and fix nested gallery directory handling
#    - add back/up link to child/nested galleries
#    - gallery page html and styling
#    - create index.html in html/gals/ with list of galleries?
#    - list galleries sorted according to the subdirectories they are in
# - use file descriptors for: warning, errors, verbose, very-verbose, debug, log - use predate function for logfile, e.g. exec 5> >(predate|tee --append "${logfile}") or something like that
# - let option --force/-F take an argument: number of errors to ignore - that way if one error is encountered, user can decide to run it again ignoring just that error
# - idea for when caching will be used and tested more:
#     - in functions that either generate new cache or use existing cache, include a hook that decides whether the cache should be re-generated even if it exists
#     - if the hook returns anything other than 0, the item/chunk/cache will be re-generated
#     - think of possible use cases and make sure the information needed for the hook to make that decision are accessible by the hook
# - use the part of the file name after the dash in file names of image attachment files as indicator where the img tag should be placed
# - check file format when generating desired_(something)
# - problem?: If script is interupted before it can error-exit with 7 (dir is locked), clean-up will remove the lock that was set by the other script
#     - accepted for now - this could be solved by storing the pid of the process that created the lock file in the lock file and only let that process delete the file
#
################################################################################################################################################################
