- Implement flexible theme switching via site.conf (site_theme, site_theme_css_file). - Ensure correct copying of theme static assets, with theme assets overriding root assets. - Resolve CSS linking issues by checking file existence after static copy and using correct paths for Pandoc. - Refactor path construction to prevent duplication when using absolute/relative output paths. - Create comprehensive how-it-works.md detailing system architecture, theme creation, and overall workflow. - Clarify design philosophy: qsgen3 remains design-agnostic, only linking main theme CSS automatically.
1111 lines
49 KiB
Bash
Executable File
1111 lines
49 KiB
Bash
Executable File
#!/usr/bin/env zsh
|
|
|
|
# qsgen3 - A Minimal Markdown Static Site Generator
|
|
# Version: 0.1.0
|
|
QSGEN_NAME="qsgen3"
|
|
VERSION="0.2.0"
|
|
# Author: Your Name/AI Assistant
|
|
|
|
# Strict mode
|
|
set -euo pipefail
|
|
|
|
# Locale and umask for robustness and security
|
|
LC_ALL=C
|
|
LANG=C
|
|
umask 0022
|
|
|
|
# --- Configuration ---
|
|
# Associative array to hold configuration values
|
|
typeset -A QSG_CONFIG
|
|
|
|
|
|
# --- Script Paths ---
|
|
# Get the directory where the script is located - SCRIPT_DIR might still be useful for finding layouts if not overridden by config
|
|
SCRIPT_DIR="$(cd "$(dirname "${(%):-%x}")" && pwd)"
|
|
# Project root is the current working directory
|
|
PROJECT_ROOT="$PWD"
|
|
# Default config file name; full path will be resolved in main after option parsing
|
|
CONFIG_FILE_NAME="site.conf"
|
|
# Full path to the configuration file, to be determined in main
|
|
CONFIG_FILE=""
|
|
|
|
# --- Usage Information ---
|
|
_usage() {
|
|
cat <<EOF
|
|
Usage: $QSGEN_NAME [options]
|
|
|
|
$QSGEN_NAME - A Minimal Markdown Static Site Generator (Version $VERSION)
|
|
|
|
Options:
|
|
-c, --config <file> Specify a custom configuration file.
|
|
(Default: site.conf in the project root)
|
|
-V, --version Show version information and exit.
|
|
-h, --help Show this help message and exit.
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# --- Logging ---
|
|
_log() {
|
|
local level=$1
|
|
shift
|
|
local message=$*
|
|
# ANSI color codes
|
|
local color_normal="\033[0m"
|
|
local color_red="\033[0;31m"
|
|
local color_green="\033[0;32m"
|
|
local color_yellow="\033[0;33m"
|
|
local color_blue="\033[0;34m"
|
|
local color_cyan="\033[0;36m"
|
|
|
|
# QSG_DEBUG can be set to true (e.g. export QSG_DEBUG=true) to enable debug logs
|
|
# By default, it's off unless explicitly set.
|
|
: ${QSG_DEBUG:=false}
|
|
|
|
# Prepare format string and arguments for printf
|
|
# The message itself is %s to prevent it from being interpreted as a format string.
|
|
local format_string="%s[%s]%s %s\n"
|
|
local color_code=""
|
|
local log_prefix=""
|
|
local output_stream=1 # stdout by default
|
|
|
|
case $level in
|
|
DEBUG)
|
|
if [[ "$QSG_DEBUG" != "true" ]]; then return 0; fi
|
|
color_code="$color_cyan"
|
|
log_prefix="DEBUG"
|
|
;;
|
|
INFO)
|
|
color_code="$color_blue"
|
|
log_prefix="INFO"
|
|
;;
|
|
SUCCESS)
|
|
color_code="$color_green"
|
|
log_prefix="SUCCESS"
|
|
;;
|
|
WARNING)
|
|
color_code="$color_yellow"
|
|
log_prefix="WARNING"
|
|
output_stream=2 # stderr
|
|
;;
|
|
ERROR)
|
|
color_code="$color_red"
|
|
log_prefix="ERROR"
|
|
output_stream=2 # stderr
|
|
;;
|
|
*)
|
|
color_code="$color_red"
|
|
log_prefix="LOG_ERROR"
|
|
message="Unknown log level: $level. Original Message: $message"
|
|
output_stream=2 # stderr
|
|
;;
|
|
esac
|
|
|
|
if [[ $output_stream -eq 1 ]]; then
|
|
printf "$format_string" "$color_code" "$log_prefix" "$color_normal" "$message"
|
|
else
|
|
printf "$format_string" "$color_code" "$log_prefix" "$color_normal" "$message" >&2
|
|
fi
|
|
}
|
|
|
|
# --- YAML Helper Functions ---
|
|
_yaml_escape_val() {
|
|
# Escape backslashes and double quotes for YAML string values
|
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
}
|
|
|
|
# --- Configuration Loading ---
|
|
_load_config() {
|
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
_log ERROR "Configuration file not found: $CONFIG_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
_log INFO "Attempting to load configuration from: $CONFIG_FILE"
|
|
local current_section=""
|
|
local line_num=0
|
|
|
|
# Check if config file is readable
|
|
if [[ ! -r "$CONFIG_FILE" ]]; then
|
|
_log ERROR "Configuration file is not readable: $CONFIG_FILE"
|
|
exit 1
|
|
else
|
|
_log DEBUG "Configuration file is readable: $CONFIG_FILE"
|
|
fi
|
|
|
|
_log DEBUG "Attempting to enter config parsing loop. Disabling set -e temporarily."
|
|
set +e # Temporarily disable exit on error
|
|
# Read file line by line
|
|
while IFS= read -r line; do
|
|
((line_num++))
|
|
# _log DEBUG "Read raw line $line_num: '$line'" # Raw line logging can be noisy, comment for now
|
|
|
|
# Remove leading/trailing whitespace
|
|
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
# _log DEBUG "Trimmed line $line_num: '$line'"
|
|
|
|
# Skip empty lines and comments (lines starting with # or ;)
|
|
if [[ -z "$line" ]] || [[ "$line" == \#* ]] || [[ "$line" == \;* ]]; then
|
|
# _log DEBUG "Skipping line $line_num (empty or comment)"
|
|
continue
|
|
fi
|
|
|
|
# Check for section headers like [section_name]
|
|
if [[ "$line" == \[[a-zA-Z0-9_]+]\] ]]; then
|
|
current_section=$(echo "$line" | sed 's/\[//;s/\]//')
|
|
# _log DEBUG "Switched to section: $current_section"
|
|
QSG_CONFIG[${current_section}_exists]=true # Mark section as existing
|
|
continue
|
|
fi
|
|
|
|
# Process key-value pairs within a section
|
|
if [[ -n "$current_section" ]] && [[ "$line" == *=* ]]; then
|
|
local key=$(echo "$line" | cut -d'=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
# Extract key and value
|
|
key="${line%%=*}"
|
|
local raw_value="${line#*=}"
|
|
|
|
# Strip trailing comments (anything after #). Ensure this is done BEFORE quote stripping.
|
|
local value_no_comment="${raw_value%%#*}"
|
|
|
|
# Remove potential quotes from value_no_comment
|
|
local value_no_quotes="${value_no_comment#\"}" # Remove leading quote
|
|
value_no_quotes="${value_no_quotes%\"}" # Remove trailing quote
|
|
|
|
# Trim leading/trailing whitespace from value_no_quotes
|
|
# Using standard parameter expansion for trimming whitespace
|
|
local temp_value="${value_no_quotes##[[:space:]]}" # Remove leading whitespace
|
|
value="${temp_value%%[[:space:]]}" # Remove trailing whitespace
|
|
local full_key="${current_section}_${key}"
|
|
QSG_CONFIG[$full_key]="$value"
|
|
# _log DEBUG "Loaded config: $full_key = $value"
|
|
elif [[ -z "$current_section" ]] && [[ "$line" == *=* ]]; then
|
|
# Handle global key-value pairs (not inside a section)
|
|
key="${line%%=*}" # Extract key
|
|
local raw_value="${line#*=}" # Extract raw value
|
|
|
|
# 1. Strip trailing comments
|
|
local value_no_comment="${raw_value%%#*}"
|
|
|
|
# 2. Trim leading/trailing whitespace from the comment-stripped value
|
|
local temp_trimmed_value="${value_no_comment##[[:space:]]}" # Remove leading whitespace
|
|
local value_trimmed="${temp_trimmed_value%%[[:space:]]}" # Remove trailing whitespace
|
|
|
|
# 3. Remove potential quotes from the trimmed, comment-stripped value
|
|
if [[ "${value_trimmed:0:1}" == "\"" && "${value_trimmed: -1}" == "\"" ]]; then
|
|
# It's double-quoted
|
|
value="${value_trimmed:1:-1}"
|
|
elif [[ "${value_trimmed:0:1}" == "'" && "${value_trimmed: -1}" == "'" ]]; then
|
|
# It's single-quoted
|
|
value="${value_trimmed:1:-1}"
|
|
else
|
|
# Not quoted or improperly quoted, use as is
|
|
value="$value_trimmed"
|
|
fi
|
|
|
|
# For specific path keys, ensure they are absolute
|
|
case "$key" in
|
|
paths_content_dir|paths_output_dir|paths_layouts_dir|paths_static_dir)
|
|
if [[ "$value" != /* && -n "$value" ]]; then # If not absolute and not empty
|
|
value="$PROJECT_ROOT/$value"
|
|
fi
|
|
;;
|
|
esac
|
|
QSG_CONFIG[$key]="$value"
|
|
# _log DEBUG "Loaded global config: $key = $value"
|
|
else
|
|
if [[ -n "$line" ]]; then # Only log if line is not empty (already handled above)
|
|
_log WARNING "Ignoring malformed line $line_num in $CONFIG_FILE: $line"
|
|
fi
|
|
fi
|
|
done < "$CONFIG_FILE"
|
|
local read_status=$?
|
|
set -e # Re-enable exit on error
|
|
_log DEBUG "Exited config parsing loop."
|
|
_log DEBUG "Exited config parsing loop. Read status: $read_status. Re-enabled set -e."
|
|
|
|
if [[ $read_status -ne 0 && $read_status -ne 1 ]]; then # read returns 1 on EOF, which is fine
|
|
_log WARNING "Potential issue reading configuration file. Read command exited with status: $read_status"
|
|
fi
|
|
|
|
# Validate required configuration
|
|
local required_config_keys=(
|
|
"paths_content_dir"
|
|
"paths_output_dir"
|
|
"paths_layouts_dir"
|
|
"paths_static_dir"
|
|
"site_name"
|
|
"site_url"
|
|
"site_theme"
|
|
)
|
|
local missing_configs=false
|
|
|
|
for key in "${required_config_keys[@]}"; do
|
|
if [[ -z "${QSG_CONFIG[$key]:-}" ]]; then
|
|
_log ERROR "Required configuration '$key' is missing from $CONFIG_FILE."
|
|
missing_configs=true
|
|
else
|
|
# Resolve path-related keys to absolute paths if not already absolute
|
|
if [[ "$key" == paths_* ]] && [[ "${QSG_CONFIG[$key]}" != /* ]]; then
|
|
QSG_CONFIG[$key]="$PROJECT_ROOT/${QSG_CONFIG[$key]}"
|
|
fi
|
|
_log DEBUG "Validated config: $key = ${QSG_CONFIG[$key]}"
|
|
fi
|
|
done
|
|
|
|
# Default build options if not set
|
|
: ${QSG_CONFIG[build_options_process_drafts]:=false}
|
|
: ${QSG_CONFIG[build_options_generate_rss]:=true}
|
|
# : ${QSG_CONFIG[build_options_generate_sitemap]:=true} # Example if sitemap is added
|
|
|
|
_log DEBUG "build_options_process_drafts set to: ${QSG_CONFIG[build_options_process_drafts]}"
|
|
_log DEBUG "build_options_generate_rss set to: ${QSG_CONFIG[build_options_generate_rss]}"
|
|
# _log DEBUG "build_options_generate_sitemap set to: ${QSG_CONFIG[build_options_generate_sitemap]}"
|
|
|
|
if [[ "$missing_configs" == true ]]; then
|
|
_log ERROR "One or more required configurations are missing. Please check $CONFIG_FILE."
|
|
exit 1
|
|
fi
|
|
|
|
_log INFO "Configuration loaded successfully."
|
|
return 0
|
|
}
|
|
|
|
# --- Dependency Checking ---
|
|
_check_dependencies() {
|
|
_log INFO "Checking dependencies..."
|
|
if ! command -v pandoc &> /dev/null; then
|
|
_log ERROR "Pandoc is not installed. Please install pandoc to continue."
|
|
_log ERROR "See: https://pandoc.org/installing.html"
|
|
exit 1
|
|
fi
|
|
_log INFO "All critical dependencies found."
|
|
}
|
|
|
|
# --- Core Functions ---
|
|
_clean_output_dir() {
|
|
_log INFO "Cleaning output directory: ${QSG_CONFIG[paths_output_dir]}"
|
|
# rm -rf "${QSG_CONFIG[paths_output_dir]}"/* # Be careful with this!
|
|
# For now, just ensure it exists
|
|
mkdir -p "${QSG_CONFIG[paths_output_dir]}"
|
|
}
|
|
|
|
_copy_static_files() {
|
|
local root_static_source_dir_rel="${QSG_CONFIG[paths_static_dir]:-}" # Relative path like "static"
|
|
local root_static_source_dir_abs=""
|
|
if [[ -n "$root_static_source_dir_rel" ]]; then
|
|
root_static_source_dir_abs="$PROJECT_ROOT/$root_static_source_dir_rel"
|
|
fi
|
|
|
|
local theme_static_source_dir_abs="${QSG_CONFIG[theme_static_source_dir]:-}" # Absolute path, set in main
|
|
|
|
# Target directory for all static files in the output.
|
|
local output_dir_from_config_csf="${QSG_CONFIG[paths_output_dir]:-output}" # Default to "output" if not set in _copy_static_files
|
|
local base_output_dir_abs_csf=""
|
|
if [[ "$output_dir_from_config_csf" == /* ]]; then
|
|
base_output_dir_abs_csf="$output_dir_from_config_csf"
|
|
else
|
|
base_output_dir_abs_csf="$PROJECT_ROOT/$output_dir_from_config_csf"
|
|
fi
|
|
local overall_target_static_dir_abs="$base_output_dir_abs_csf/static"
|
|
|
|
_log INFO "Preparing to copy static files to $overall_target_static_dir_abs"
|
|
if ! mkdir -p "$overall_target_static_dir_abs"; then
|
|
_log ERROR "Failed to create target static directory: $overall_target_static_dir_abs"
|
|
return 1
|
|
fi
|
|
|
|
local rsync_common_opts=(-a --delete --exclude '.gitkeep' --exclude '.DS_Store')
|
|
local cp_common_opts=(-R -p) # -p to preserve mode, ownership, timestamps
|
|
local copy_cmd=""
|
|
local cmd_opts=()
|
|
|
|
if command -v rsync &> /dev/null; then
|
|
copy_cmd="rsync"
|
|
cmd_opts=("${rsync_common_opts[@]}")
|
|
else
|
|
copy_cmd="cp"
|
|
# For cp, source needs trailing /., target needs to exist or be a dir name
|
|
cmd_opts=("${cp_common_opts[@]}")
|
|
_log WARNING "rsync not found. Falling back to 'cp'. Some features like --delete might not be available."
|
|
fi
|
|
|
|
local root_copy_ok=true
|
|
local theme_copy_ok=true
|
|
local any_files_copied=false
|
|
|
|
# 1. Copy from root static directory (if it exists)
|
|
if [[ -n "$root_static_source_dir_abs" && -d "$root_static_source_dir_abs" ]]; then
|
|
_log DEBUG "Copying from root static directory: $root_static_source_dir_abs using $copy_cmd"
|
|
local source_path_for_cmd="$root_static_source_dir_abs/"
|
|
local target_path_for_cmd="$overall_target_static_dir_abs/"
|
|
if [[ "$copy_cmd" == "cp" ]]; then source_path_for_cmd="$root_static_source_dir_abs/."; fi
|
|
|
|
if "$copy_cmd" "${cmd_opts[@]}" "$source_path_for_cmd" "$target_path_for_cmd"; then
|
|
_log DEBUG "Successfully copied files from $root_static_source_dir_abs"
|
|
any_files_copied=true
|
|
else
|
|
_log ERROR "Failed to copy files from $root_static_source_dir_abs to $overall_target_static_dir_abs using $copy_cmd"
|
|
root_copy_ok=false
|
|
fi
|
|
else
|
|
_log DEBUG "Root static directory '$root_static_source_dir_abs' not found or not specified. Skipping."
|
|
fi
|
|
|
|
# 2. Copy from theme static directory (if it exists), overwriting root static files
|
|
if [[ -n "$theme_static_source_dir_abs" && -d "$theme_static_source_dir_abs" ]]; then
|
|
_log DEBUG "Copying from theme static directory (will overwrite): $theme_static_source_dir_abs using $copy_cmd"
|
|
local source_path_for_cmd="$theme_static_source_dir_abs/"
|
|
local target_path_for_cmd="$overall_target_static_dir_abs/"
|
|
if [[ "$copy_cmd" == "cp" ]]; then source_path_for_cmd="$theme_static_source_dir_abs/."; fi
|
|
|
|
if "$copy_cmd" "${cmd_opts[@]}" "$source_path_for_cmd" "$target_path_for_cmd"; then
|
|
_log DEBUG "Successfully copied files from $theme_static_source_dir_abs"
|
|
any_files_copied=true
|
|
else
|
|
_log ERROR "Failed to copy files from $theme_static_source_dir_abs to $overall_target_static_dir_abs using $copy_cmd"
|
|
theme_copy_ok=false
|
|
fi
|
|
else
|
|
_log DEBUG "Theme static source directory '$theme_static_source_dir_abs' not found or not specified. Skipping."
|
|
fi
|
|
|
|
if ! $root_copy_ok || ! $theme_copy_ok; then
|
|
_log ERROR "One or more static file copy operations failed."
|
|
return 1
|
|
fi
|
|
|
|
if ! $any_files_copied; then
|
|
_log INFO "No static files found to copy (neither root nor theme static directories were specified, existed, or contained files)."
|
|
else
|
|
_log INFO "Static files copied successfully to $overall_target_static_dir_abs"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
_generate_index_page() {
|
|
_log DEBUG "Entered _generate_index_page"
|
|
|
|
local temp_yaml_file
|
|
temp_yaml_file=$(mktemp -t qsgen3_index_yaml.XXXXXX)
|
|
if [[ $? -ne 0 || -z "$temp_yaml_file" || ! -f "$temp_yaml_file" ]]; then
|
|
_log ERROR "Failed to create temporary file for index YAML. Exiting."
|
|
return 1
|
|
fi
|
|
trap "_log DEBUG \"Cleaning up temporary index YAML file: ${temp_yaml_file}\"; rm -f \"${temp_yaml_file}\"; trap - EXIT INT TERM HUP" EXIT INT TERM HUP
|
|
_log DEBUG "Created temporary YAML file for index: $temp_yaml_file"
|
|
_log INFO "Generating index page..."
|
|
local content_posts_dir="${QSG_CONFIG[paths_content_dir]}/posts"
|
|
local output_index_file="${QSG_CONFIG[paths_output_dir]}/index.html"
|
|
local layout_index_file="${QSG_CONFIG[paths_layouts_dir]}/index.html"
|
|
|
|
if [[ ! -d "$content_posts_dir" ]]; then
|
|
_log WARNING "Posts directory not found: $content_posts_dir. Skipping index page generation."
|
|
return 0
|
|
fi
|
|
|
|
if [[ ! -f "$layout_index_file" ]]; then
|
|
_log ERROR "Index layout file not found: $layout_index_file. Cannot generate index page."
|
|
return 1
|
|
fi
|
|
|
|
local post_details=()
|
|
|
|
# Collect post details
|
|
# This is a simplified approach. Robust YAML parsing is complex in pure shell.
|
|
# We'll extract title, date, and construct a URL.
|
|
# We assume date format is YYYY-MM-DD for sorting.
|
|
# A 'summary' field in frontmatter would also be good.
|
|
|
|
local md_file
|
|
for md_file in $(find "$content_posts_dir" -name '*.md' -type f); do
|
|
_log DEBUG "Processing metadata for $md_file"
|
|
|
|
# Extract frontmatter (lines between --- and ---)
|
|
# Use sed to print lines between the first '---' and the next '---'
|
|
# Then grep for title, date, summary, draft
|
|
local frontmatter=$(sed -n '/^---$/,/^---$/{/^---$/d;p;}' "$md_file")
|
|
|
|
if [[ -z "$frontmatter" ]]; then
|
|
_log WARNING "No YAML frontmatter found or frontmatter is empty in $md_file. Skipping."
|
|
continue
|
|
fi
|
|
|
|
# Extract title, date, summary, and draft from frontmatter
|
|
# Basic extraction: assumes 'key: value' format, strips quotes
|
|
local title=$(echo "$frontmatter" | grep -m1 -iE '^title:' | sed -E 's/^title:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local date=$(echo "$frontmatter" | grep -m1 -iE '^date:' | sed -E 's/^date:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local summary=$(echo "$frontmatter" | grep -m1 -iE '^summary:' | sed -E 's/^summary:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local draft=$(echo "$frontmatter" | grep -m1 -iE '^draft:' | sed -E 's/^draft:[[:space:]]*//i' | tr '[:upper:]' '[:lower:]')
|
|
|
|
_log DEBUG "Extracted Title: [$title], Date: [$date], Summary: [$summary], Draft: [$draft] for $md_file"
|
|
|
|
if [[ "$draft" == "true" && "${QSG_CONFIG[build_options_process_drafts]}" != "true" ]]; then
|
|
_log DEBUG "Skipping draft post for index: $md_file (draft: $draft)"
|
|
continue
|
|
fi
|
|
|
|
if [[ -z "$title" ]]; then
|
|
_log WARNING "Post '$md_file' is missing a title in frontmatter. Skipping for index."
|
|
continue
|
|
fi
|
|
|
|
# Construct URL relative to site root
|
|
local relative_path="${md_file#${QSG_CONFIG[paths_content_dir]}/}" # e.g., posts/hello-world.md
|
|
local post_url="${QSG_CONFIG[site_url]}/${relative_path%.md}.html"
|
|
|
|
# Use date for sorting, default to a very old date if not found
|
|
local sort_key="${date:-0000-00-00}"
|
|
post_details+=("${sort_key}|${title}|${post_url}|${date:-N/A}|${summary:-}")
|
|
done
|
|
|
|
# Sort posts by date (descending)
|
|
# IFS=$'\n' sorted_posts=($(printf "%s\n" "${post_details[@]}" | sort -t'|' -k1,1r -k2,2))
|
|
# Using a loop for sorting to avoid IFS issues and handle empty array better
|
|
local sorted_posts=()
|
|
if [[ ${#post_details[@]} -gt 0 ]]; then
|
|
OIFS="$IFS"
|
|
IFS=$'\n'
|
|
sorted_posts=($(printf "%s\n" "${post_details[@]}" | sort -t'|' -k1,1r))
|
|
IFS="$OIFS"
|
|
fi
|
|
|
|
# Generate comprehensive YAML string including site-wide metadata and posts
|
|
local site_title_for_yaml="${QSG_CONFIG[site_title]:-${QSG_CONFIG[site_name]}}"
|
|
|
|
# Helper for YAML string escaping (handles backslashes and double quotes)
|
|
local yaml_lines=()
|
|
yaml_lines+=( "$(printf 'current_year: "%s"' "$(date +%Y)")" )
|
|
yaml_lines+=( "$(printf 'site_name: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_name]}")")" )
|
|
yaml_lines+=( "$(printf 'site_tagline: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_tagline]}")")" )
|
|
yaml_lines+=( "$(printf 'site_url: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_url]}")")" )
|
|
yaml_lines+=( "$(printf 'title: "%s"' "$(_yaml_escape_val "$site_title_for_yaml")")" )
|
|
yaml_lines+=( "$(printf 'posts:')" )
|
|
|
|
if [[ ${#sorted_posts[@]} -eq 0 ]]; then
|
|
_log DEBUG "No posts found to add to index YAML."
|
|
yaml_lines+=( "$(printf ' [] # Explicitly empty list')" )
|
|
else
|
|
for detail_line in "${sorted_posts[@]}"; do
|
|
# Original detail_line format: "${sort_key}|${title}|${post_url}|${date:-N/A}|${summary:-}"
|
|
# Using Zsh specific split for robustness with special characters in fields
|
|
local parts_array=( ${(s[|])detail_line} )
|
|
|
|
local post_title_val="${parts_array[2]}"
|
|
local post_url_val="${parts_array[3]}"
|
|
local post_date_val="${parts_array[4]}"
|
|
local post_summary_val="${parts_array[5]}"
|
|
|
|
yaml_lines+=( "$(printf ' - title: "%s"' "$(_yaml_escape_val "$post_title_val")")" )
|
|
yaml_lines+=( "$(printf ' url: "%s"' "$(_yaml_escape_val "$post_url_val")")" )
|
|
yaml_lines+=( "$(printf ' date: "%s"' "$(_yaml_escape_val "$post_date_val")")" )
|
|
yaml_lines+=( "$(printf ' summary: "%s"' "$(_yaml_escape_val "$post_summary_val")")" )
|
|
done
|
|
fi
|
|
|
|
local temp_yaml_content
|
|
temp_yaml_content="$(IFS=$'\n'; echo "${yaml_lines[*]}")"
|
|
temp_yaml_content+=$'\n' # Ensure a final trailing newline for the whole block
|
|
_log DEBUG "Generated comprehensive YAML for index (first 300 chars):\n${temp_yaml_content:0:300}..."
|
|
|
|
# Write YAML to temporary file
|
|
_log DEBUG "Full comprehensive YAML for index before writing to file (raw, first 1000 chars to avoid excessive logging):\n${temp_yaml_content:0:1000}"
|
|
|
|
if ! printf '%s' "$temp_yaml_content" > "$temp_yaml_file"; then
|
|
_log ERROR "Failed to write comprehensive YAML to temporary file: $temp_yaml_file"
|
|
return 1 # Trap will clean up temp_yaml_file
|
|
fi
|
|
_log DEBUG "Successfully wrote comprehensive YAML to $temp_yaml_file"
|
|
|
|
|
|
# Theme CSS handling for index page
|
|
local source_theme_css_file="${QSG_CONFIG[paths_layouts_dir]}/css/${QSG_CONFIG[site_theme]}.css"
|
|
local target_theme_css_dir="${QSG_CONFIG[paths_output_dir]}/css"
|
|
local target_theme_css_file="$target_theme_css_dir/theme.css"
|
|
local pandoc_css_path_for_index="/css/theme.css"
|
|
|
|
_log DEBUG "In _generate_index_page: PROJECT_ROOT='$PROJECT_ROOT'"
|
|
local pandoc_cmd_index=(
|
|
pandoc
|
|
"--metadata-file" "$temp_yaml_file"
|
|
)
|
|
|
|
# Add CSS if specified (pandoc_css_path_arg is derived in main)
|
|
if [[ -n "${QSG_CONFIG[pandoc_css_path_arg]:-}" ]]; then
|
|
pandoc_cmd_index+=("--css" "${QSG_CONFIG[pandoc_css_path_arg]}")
|
|
_log DEBUG "Using CSS for index: ${QSG_CONFIG[pandoc_css_path_arg]}"
|
|
else
|
|
_log DEBUG "No CSS specified for index page."
|
|
fi
|
|
|
|
# Add remaining Pandoc options for index page
|
|
pandoc_cmd_index+=(
|
|
"--template=${layout_index_file}"
|
|
"--output=${output_index_file}"
|
|
"--standalone"
|
|
)
|
|
|
|
_log INFO "Generating $output_index_file using template $layout_index_file with YAML from $temp_yaml_file"
|
|
|
|
_log DEBUG "Pandoc command for index page will be constructed as follows:"
|
|
_log DEBUG "Base command: pandoc"
|
|
_log DEBUG "CSS arg if theme applies: --css ${pandoc_css_path_for_index}"
|
|
_log DEBUG "Metadata file arg: --metadata-file ${temp_yaml_file}"
|
|
_log DEBUG "Template arg: --template=${layout_index_file}"
|
|
_log DEBUG "Output arg: --output=${output_index_file}"
|
|
_log DEBUG "Other args: --standalone"
|
|
_log DEBUG "Final pandoc_cmd_index array: ${(q+)pandoc_cmd_index}"
|
|
|
|
local pandoc_run_stderr_file
|
|
pandoc_run_stderr_file=$(mktemp -t pandoc_stderr_index.XXXXXX)
|
|
if [[ -z "$pandoc_run_stderr_file" || ! -e "$pandoc_run_stderr_file" ]]; then # -e to check existence, could be pipe not file
|
|
_log CRITICAL "Failed to create temporary file for Pandoc stderr (index). mktemp failed."
|
|
# Attempt to clean up the main temp YAML file before exiting, if it was created.
|
|
if [[ -n "$temp_yaml_file" && -f "$temp_yaml_file" ]]; then
|
|
_log DEBUG "Cleaning up temporary index YAML file ($temp_yaml_file) due to mktemp failure for stderr file."
|
|
rm -f "$temp_yaml_file"
|
|
fi
|
|
return 1
|
|
fi
|
|
# This temp stderr file is short-lived, trap for it isn't strictly necessary if cleaned up reliably.
|
|
|
|
local pandoc_exit_code=0
|
|
# Execute Pandoc, redirecting its stderr to the temp file
|
|
_log DEBUG "Executing Pandoc command for index page with set -x..."
|
|
set -x # Enable command tracing
|
|
if "${pandoc_cmd_index[@]}" 2> "$pandoc_run_stderr_file"; then
|
|
pandoc_exec_status=0
|
|
else
|
|
pandoc_exec_status=$?
|
|
fi
|
|
set +x # Disable command tracing
|
|
_log DEBUG "Pandoc execution finished. Original exit status from Pandoc: $pandoc_exec_status"
|
|
pandoc_exit_code=$pandoc_exec_status # Use the captured status
|
|
|
|
local stderr_content=""
|
|
if [[ -s "$pandoc_run_stderr_file" ]]; then
|
|
stderr_content=$(<"$pandoc_run_stderr_file")
|
|
fi
|
|
rm -f "$pandoc_run_stderr_file"
|
|
|
|
if [[ $pandoc_exit_code -ne 0 ]]; then
|
|
_log ERROR "Pandoc command failed for '$output_index_file' with exit code: $pandoc_exit_code."
|
|
if [[ -n "$stderr_content" ]]; then
|
|
_log ERROR "Pandoc stderr:\n$stderr_content"
|
|
fi
|
|
_log ERROR "YAML content passed to Pandoc was in: $temp_yaml_file"
|
|
_log ERROR "YAML content dump (first 500 chars):\n$(head -c 500 "$temp_yaml_file" 2>/dev/null || echo 'Failed to read YAML dump')"
|
|
return $pandoc_exit_code # Return Pandoc's non-zero exit code
|
|
elif [[ -n "$stderr_content" ]]; then
|
|
# Pandoc exited 0 but wrote to stderr. Check for known fatal error patterns.
|
|
if echo "$stderr_content" | grep -q -iE 'YAML parse exception|template error|could not find|error reading file'; then
|
|
_log ERROR "Pandoc reported a critical error for '$output_index_file' (exit code 0). Treating as failure."
|
|
_log ERROR "Pandoc stderr:\n$stderr_content"
|
|
_log ERROR "YAML content passed to Pandoc was in: $temp_yaml_file"
|
|
_log ERROR "YAML content dump (first 500 chars):\n$(head -c 500 "$temp_yaml_file" 2>/dev/null || echo 'Failed to read YAML dump')"
|
|
return 1 # Force a failure status
|
|
else
|
|
_log WARNING "Pandoc succeeded for '$output_index_file' (exit code 0) but produced stderr (non-critical):\n$stderr_content"
|
|
# Continue, as it might be a non-fatal warning from Pandoc.
|
|
fi
|
|
else
|
|
_log DEBUG "Successfully generated $output_index_file"
|
|
fi
|
|
|
|
return 0 # If we reached here, it's success or a warning we decided to ignore.
|
|
}
|
|
|
|
_process_markdown_files() {
|
|
_log DEBUG "Entered _process_markdown_files"
|
|
_log INFO "Processing Markdown files..."
|
|
|
|
local content_dir="${QSG_CONFIG[paths_content_dir]}"
|
|
local output_dir="${QSG_CONFIG[paths_output_dir]}"
|
|
local layouts_dir="${QSG_CONFIG[paths_layouts_dir]}"
|
|
local default_post_template="$layouts_dir/post.html"
|
|
local default_page_template="$layouts_dir/page.html"
|
|
|
|
if [[ ! -f "$default_post_template" ]]; then
|
|
_log WARNING "Default post template not found: $default_post_template. Posts may not render correctly."
|
|
fi
|
|
if [[ ! -f "$default_page_template" ]]; then
|
|
_log WARNING "Default page template not found: $default_page_template. Pages may not render correctly."
|
|
fi
|
|
|
|
# Find all markdown files and process them
|
|
while IFS= read -r source_file; do
|
|
if [[ -z "$source_file" ]]; then continue; fi
|
|
_log DEBUG "Processing Markdown file: $source_file"
|
|
|
|
local relative_path="${source_file#$content_dir/}"
|
|
if [[ "$content_dir" == "$source_file" ]]; then
|
|
relative_path=$(basename "$source_file")
|
|
elif [[ "$content_dir" == "/" && "$source_file" == /* ]]; then
|
|
relative_path="${source_file#/}"
|
|
elif [[ "$content_dir" == "." && "$source_file" == ./* ]]; then
|
|
relative_path="${source_file#./}"
|
|
fi
|
|
|
|
local output_file_html_part="${relative_path%.md}.html"
|
|
local output_file_abs="$output_dir/$output_file_html_part"
|
|
|
|
mkdir -p "$(dirname "$output_file_abs")"
|
|
|
|
local frontmatter=$(sed -n '/^---$/,/^---$/{/^---$/d;p;}' "$source_file")
|
|
local title=$(echo "$frontmatter" | grep -m1 -iE '^title:' | sed -E 's/^title:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local date=$(echo "$frontmatter" | grep -m1 -iE '^date:' | sed -E 's/^date:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local draft=$(echo "$frontmatter" | grep -m1 -iE '^draft:' | sed -E 's/^draft:[[:space:]]*//i' | tr '[:upper:]' '[:lower:]')
|
|
local custom_layout_fm=$(echo "$frontmatter" | grep -m1 -iE '^layout:' | sed -E 's/^layout:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
|
|
if [[ "$draft" == "true" && "${QSG_CONFIG[build_options_process_drafts]}" != "true" ]]; then
|
|
_log DEBUG "Skipping draft file: $source_file"
|
|
continue
|
|
fi
|
|
|
|
if [[ -z "$title" ]]; then
|
|
_log WARNING "Markdown file '$source_file' is missing a title. Using filename as fallback."
|
|
local fn_no_ext=$(basename "$source_file")
|
|
title="${fn_no_ext%.md}"
|
|
fi
|
|
|
|
local template_to_use=""
|
|
if [[ -n "$custom_layout_fm" ]]; then
|
|
if [[ "$custom_layout_fm" != *.html && "$custom_layout_fm" != *.xml ]]; then custom_layout_fm+=".html"; fi
|
|
template_to_use="$layouts_dir/$custom_layout_fm"
|
|
if [[ ! -f "$template_to_use" ]]; then
|
|
_log WARNING "Custom layout '$custom_layout_fm' from '$source_file' not found at '$template_to_use'. Falling back."
|
|
template_to_use=""
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$template_to_use" ]]; then
|
|
if [[ "$relative_path" == posts/* && -f "$default_post_template" ]]; then
|
|
template_to_use="$default_post_template"
|
|
elif [[ -f "$default_page_template" ]]; then
|
|
template_to_use="$default_page_template"
|
|
elif [[ -f "$default_post_template" ]]; then
|
|
_log DEBUG "Default page template missing, falling back to default post template for $source_file"
|
|
template_to_use="$default_post_template"
|
|
else
|
|
_log ERROR "No suitable default template found (post.html or page.html) for '$source_file'. Skipping."
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -f "$template_to_use" ]]; then
|
|
_log ERROR "Template '$template_to_use' for '$source_file' is not a valid file. Skipping."
|
|
continue
|
|
fi
|
|
|
|
local pandoc_cmd=(
|
|
pandoc "$source_file"
|
|
--to html5
|
|
--output "$output_file_abs"
|
|
--standalone
|
|
--template "$template_to_use"
|
|
)
|
|
|
|
pandoc_cmd+=(--metadata "title=$title")
|
|
if [[ -n "$date" ]]; then
|
|
pandoc_cmd+=(--metadata "date=$date")
|
|
fi
|
|
for key val in "${(@kv)QSG_CONFIG}"; do
|
|
if [[ "$key" == site_* || "$key" == author_* ]]; then
|
|
pandoc_cmd+=(--metadata "$key=$val")
|
|
fi
|
|
done
|
|
pandoc_cmd+=(--metadata "current_year=$(date +%Y)")
|
|
|
|
# Add CSS if specified (pandoc_css_path_arg is derived in main)
|
|
if [[ -n "${QSG_CONFIG[pandoc_css_path_arg]:-}" ]]; then
|
|
pandoc_cmd+=("--css" "${QSG_CONFIG[pandoc_css_path_arg]}")
|
|
_log DEBUG "Using CSS for post $source_file: ${QSG_CONFIG[pandoc_css_path_arg]}"
|
|
else
|
|
_log DEBUG "No CSS specified for post $source_file."
|
|
fi
|
|
|
|
_log INFO "Generating $output_file_abs from $source_file using template $template_to_use"
|
|
if ! "${pandoc_cmd[@]}"; then
|
|
_log ERROR "Pandoc failed for $source_file. Command was: ${pandoc_cmd[*]}"
|
|
else
|
|
_log DEBUG "Successfully generated $output_file_abs"
|
|
fi
|
|
done < <(find "$content_dir" -type f -name '*.md' -print0 | xargs -0 -r realpath)
|
|
|
|
_log INFO "Finished processing Markdown files."
|
|
return 0
|
|
}
|
|
|
|
_generate_rss_feed() {
|
|
_log DEBUG "Entered _generate_rss_feed"
|
|
|
|
local temp_rss_yaml_file
|
|
temp_rss_yaml_file=$(mktemp -t qsgen3_rss_yaml.XXXXXX)
|
|
if [[ $? -ne 0 || -z "$temp_rss_yaml_file" || ! -f "$temp_rss_yaml_file" ]]; then
|
|
_log ERROR "Failed to create temporary file for RSS YAML. Exiting."
|
|
return 1
|
|
fi
|
|
trap "_log DEBUG \"Cleaning up temporary RSS YAML file: ${temp_rss_yaml_file}\"; rm -f \"${temp_rss_yaml_file}\"; trap - EXIT INT TERM HUP" EXIT INT TERM HUP
|
|
_log DEBUG "Created temporary YAML file for RSS: $temp_rss_yaml_file"
|
|
if [[ "${QSG_CONFIG[build_options_generate_rss]}" != "true" ]]; then
|
|
_log INFO "RSS feed generation is disabled in configuration. Skipping."
|
|
return 0
|
|
fi
|
|
|
|
_log INFO "Generating RSS feed..."
|
|
local content_posts_dir="${QSG_CONFIG[paths_content_dir]}/posts"
|
|
local output_rss_file="${QSG_CONFIG[paths_output_dir]}/rss.xml"
|
|
local layout_rss_file="${QSG_CONFIG[paths_layouts_dir]}/rss.xml"
|
|
|
|
if [[ ! -d "$content_posts_dir" ]]; then
|
|
_log WARNING "Posts directory for RSS ('$content_posts_dir') not found. Cannot generate RSS feed."
|
|
return 0
|
|
fi
|
|
|
|
if [[ ! -f "$layout_rss_file" ]]; then
|
|
_log ERROR "RSS layout file not found: $layout_rss_file. Cannot generate RSS feed."
|
|
return 1
|
|
fi
|
|
|
|
local post_details=()
|
|
local md_file
|
|
# Use find with xargs and realpath for robust path handling, then loop
|
|
while IFS= read -r md_file; do
|
|
if [[ -z "$md_file" ]]; then continue; fi
|
|
_log DEBUG "Processing $md_file for RSS feed."
|
|
|
|
local frontmatter=$(sed -n '/^---$/,/^---$/{/^---$/d;p;}' "$md_file")
|
|
if [[ -z "$frontmatter" ]]; then
|
|
_log DEBUG "No YAML frontmatter found in $md_file for RSS. Skipping."
|
|
continue
|
|
fi
|
|
|
|
local title=$(echo "$frontmatter" | grep -m1 -iE '^title:' | sed -E 's/^title:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local date_iso=$(echo "$frontmatter" | grep -m1 -iE '^date:' | sed -E 's/^date:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local summary=$(echo "$frontmatter" | grep -m1 -iE '^summary:' | sed -E 's/^summary:[[:space:]]*//i; s/^["\x27](.*)["\x27]$/\1/')
|
|
local draft=$(echo "$frontmatter" | grep -m1 -iE '^draft:' | sed -E 's/^draft:[[:space:]]*//i' | tr '[:upper:]' '[:lower:]')
|
|
|
|
if [[ "$draft" == "true" && "${QSG_CONFIG[build_options_process_drafts]}" != "true" ]]; then
|
|
_log DEBUG "Skipping draft post for RSS: $md_file"
|
|
continue
|
|
fi
|
|
if [[ -z "$title" ]]; then
|
|
_log WARNING "Post '$md_file' is missing a title. Skipping for RSS."
|
|
continue
|
|
fi
|
|
if [[ -z "$date_iso" ]]; then
|
|
_log WARNING "Post '$md_file' is missing a date. Skipping for RSS."
|
|
continue
|
|
fi
|
|
|
|
local relative_path_from_content_root="${md_file#${QSG_CONFIG[paths_content_dir]}/}"
|
|
local post_url="${QSG_CONFIG[site_url]}/${relative_path_from_content_root%.md}.html"
|
|
|
|
local rfc822_date
|
|
# Attempt to convert date to RFC-822 format for RSS
|
|
# GNU date syntax for -d and -R
|
|
if rfc822_date=$(date -d "$date_iso" +'%a, %d %b %Y %H:%M:%S %z' 2>/dev/null); then
|
|
_log DEBUG "Converted date '$date_iso' to RFC-822 '$rfc822_date' for $md_file"
|
|
else
|
|
_log WARNING "Could not convert date '$date_iso' to RFC-822 for $md_file. Using original."
|
|
rfc822_date="$date_iso" # Fallback to original if conversion fails
|
|
fi
|
|
|
|
local sort_key="${date_iso:-0000-00-00}" # Use ISO date for sorting
|
|
post_details+=("${sort_key}|${title}|${post_url}|${rfc822_date}|${summary:-No summary available.}")
|
|
done < <(find "$content_posts_dir" -type f -name '*.md' -print0 | xargs -0 -r realpath)
|
|
|
|
local sorted_posts=()
|
|
if [[ ${#post_details[@]} -gt 0 ]]; then
|
|
OIFS="$IFS"; IFS=$'\n'
|
|
# Sort by ISO date (first field), descending
|
|
sorted_posts=($(printf "%s\n" "${post_details[@]}" | sort -t'|' -k1,1r))
|
|
IFS="$OIFS"
|
|
fi
|
|
|
|
local yaml_lines=()
|
|
local site_title_for_feed="${QSG_CONFIG[site_title]:-${QSG_CONFIG[site_name]}}"
|
|
local feed_url="${QSG_CONFIG[site_url]}/rss.xml"
|
|
local current_rfc822_date=$(date -R)
|
|
|
|
yaml_lines+=( "$(printf 'site_name: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_name]}")")" )
|
|
yaml_lines+=( "$(printf 'site_tagline: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_tagline]}")")" )
|
|
yaml_lines+=( "$(printf 'site_url: "%s"' "$(_yaml_escape_val "${QSG_CONFIG[site_url]}")")" )
|
|
yaml_lines+=( "$(printf 'feed_url: "%s"' "$(_yaml_escape_val "$feed_url")")" )
|
|
yaml_lines+=( "$(printf 'feed_title: "%s"' "$(_yaml_escape_val "$site_title_for_feed Feed")")" ) # For <title> in RSS
|
|
yaml_lines+=( "$(printf 'current_rfc822_date: "%s"' "$(_yaml_escape_val "$current_rfc822_date")")" ) # For <pubDate> of the feed itself
|
|
yaml_lines+=( "$(printf 'current_year: "%s"' "$(date +%Y)")" )
|
|
yaml_lines+=( "$(printf 'pagetitle: "%s"' "$(_yaml_escape_val "$site_title_for_feed Feed")")" ) # For Pandoc's HTML writer
|
|
|
|
yaml_lines+=( "$(printf 'posts:')" )
|
|
if [[ ${#sorted_posts[@]} -eq 0 ]]; then
|
|
_log INFO "No posts found to include in RSS feed after filtering."
|
|
yaml_lines+=( "$(printf ' [] # Explicitly empty list')" )
|
|
else
|
|
for detail_line in "${sorted_posts[@]}"; do
|
|
local parts=( ${(s[|])detail_line} ) # Zsh specific split
|
|
local p_title="${parts[2]:-}"
|
|
local p_url="${parts[3]:-}"
|
|
local p_date_rfc822="${parts[4]:-}"
|
|
local p_summary="${parts[5]:-}"
|
|
|
|
yaml_lines+=( "$(printf ' - post_title: "%s"' "$(_yaml_escape_val "$p_title")")" )
|
|
yaml_lines+=( "$(printf ' post_url: "%s"' "$(_yaml_escape_val "$p_url")")" )
|
|
yaml_lines+=( "$(printf ' post_date_rfc822: "%s"' "$(_yaml_escape_val "$p_date_rfc822")")" )
|
|
yaml_lines+=( "$(printf ' post_summary: "%s"' "$(_yaml_escape_val "$p_summary")")" )
|
|
# Add other fields like guid if needed, e.g., using post_url as guid
|
|
yaml_lines+=( "$(printf ' post_guid: "%s"' "$(_yaml_escape_val "$p_url")")" )
|
|
done
|
|
fi
|
|
|
|
local temp_yaml_content
|
|
temp_yaml_content="$(IFS=$'\n'; echo "${yaml_lines[*]}")"
|
|
temp_yaml_content+=$'\n' # Ensure a final trailing newline
|
|
|
|
_log DEBUG "Generated comprehensive YAML for RSS (first 300 chars):\n${temp_yaml_content:0:300}..."
|
|
|
|
if ! printf '%s' "$temp_yaml_content" > "$temp_rss_yaml_file"; then
|
|
_log ERROR "Failed to write comprehensive RSS YAML to temporary file: $temp_rss_yaml_file"
|
|
return 1
|
|
fi
|
|
_log DEBUG "Successfully wrote comprehensive RSS YAML to $temp_rss_yaml_file"
|
|
|
|
local pandoc_cmd_rss=(
|
|
pandoc
|
|
"--metadata-file" "$temp_rss_yaml_file"
|
|
"--template=${layout_rss_file}"
|
|
"--output=${output_rss_file}"
|
|
"--to" "html5" # Use HTML5 writer for custom XML template processing
|
|
"--standalone"
|
|
)
|
|
_log INFO "Generating $output_rss_file using template $layout_rss_file with YAML from $temp_rss_yaml_file"
|
|
|
|
local pandoc_run_rss_stderr_file
|
|
pandoc_run_rss_stderr_file=$(mktemp -t pandoc_stderr_rss.XXXXXX)
|
|
if [[ -z "$pandoc_run_rss_stderr_file" || ! -e "$pandoc_run_rss_stderr_file" ]]; then
|
|
_log CRITICAL "Failed to create temporary file for Pandoc stderr (RSS). mktemp failed."
|
|
if [[ -n "$temp_rss_yaml_file" && -f "$temp_rss_yaml_file" ]]; then
|
|
_log DEBUG "Cleaning up temporary RSS YAML file ($temp_rss_yaml_file) due to mktemp failure for stderr file."
|
|
rm -f "$temp_rss_yaml_file"
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
local pandoc_exit_code_rss=0
|
|
# Provide a dummy input to Pandoc since content is from metadata, redirect stderr
|
|
local pandoc_exec_status_rss
|
|
if echo "<!-- RSS feed generated by qsgen3 -->" | "${pandoc_cmd_rss[@]}" 2> "$pandoc_run_rss_stderr_file"; then
|
|
pandoc_exec_status_rss=0
|
|
else
|
|
pandoc_exec_status_rss=$?
|
|
fi
|
|
pandoc_exit_code_rss=$pandoc_exec_status_rss
|
|
|
|
local stderr_content_rss=""
|
|
if [[ -s "$pandoc_run_rss_stderr_file" ]]; then
|
|
stderr_content_rss=$(<"$pandoc_run_rss_stderr_file")
|
|
fi
|
|
rm -f "$pandoc_run_rss_stderr_file"
|
|
|
|
if [[ $pandoc_exit_code_rss -ne 0 ]]; then
|
|
_log ERROR "Pandoc command failed for '$output_rss_file' with exit code: $pandoc_exit_code_rss."
|
|
if [[ -n "$stderr_content_rss" ]]; then
|
|
_log ERROR "Pandoc stderr:\n$stderr_content_rss"
|
|
fi
|
|
_log ERROR "YAML content passed to Pandoc for RSS was in: $temp_rss_yaml_file"
|
|
_log ERROR "RSS YAML content dump (first 500 chars):\n$(head -c 500 "$temp_rss_yaml_file" 2>/dev/null || echo 'Failed to read RSS YAML dump')"
|
|
return $pandoc_exit_code_rss # Return Pandoc's non-zero exit code
|
|
elif [[ -n "$stderr_content_rss" ]]; then
|
|
if echo "$stderr_content_rss" | grep -q -iE 'YAML parse exception|template error|could not find|error reading file'; then
|
|
_log ERROR "Pandoc reported a critical error for '$output_rss_file' (exit code 0). Treating as failure."
|
|
_log ERROR "Pandoc stderr:\n$stderr_content_rss"
|
|
_log ERROR "YAML content passed to Pandoc for RSS was in: $temp_rss_yaml_file"
|
|
_log ERROR "RSS YAML content dump (first 500 chars):\n$(head -c 500 "$temp_rss_yaml_file" 2>/dev/null || echo 'Failed to read RSS YAML dump')"
|
|
return 1 # Force a failure status
|
|
else
|
|
_log WARNING "Pandoc succeeded for '$output_rss_file' (exit code 0) but produced stderr (non-critical):\n$stderr_content_rss"
|
|
fi
|
|
else
|
|
_log DEBUG "Successfully generated $output_rss_file"
|
|
fi
|
|
|
|
return 0 # If we reached here, it's success or a warning we decided to ignore.
|
|
}
|
|
|
|
main() {
|
|
local custom_config_file_arg=""
|
|
|
|
# Option parsing
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
_usage
|
|
;;
|
|
-V|--version)
|
|
echo "$QSGEN_NAME Version: $VERSION"
|
|
exit 0
|
|
;;
|
|
-c|--config)
|
|
if [[ -n "$2" ]]; then
|
|
custom_config_file_arg="$2"
|
|
shift 2
|
|
else
|
|
_log ERROR "Option --config requires an argument."
|
|
_usage # Will exit
|
|
fi
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
_log ERROR "Unknown option: $1"
|
|
_usage # Will exit
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Resolve CONFIG_FILE path
|
|
# PROJECT_ROOT is already set globally to $PWD
|
|
if [[ -n "$custom_config_file_arg" ]]; then
|
|
if [[ "$custom_config_file_arg" == /* ]]; then # Absolute path
|
|
CONFIG_FILE="$custom_config_file_arg"
|
|
else # Relative path, resolve against PROJECT_ROOT
|
|
CONFIG_FILE="$PROJECT_ROOT/$custom_config_file_arg"
|
|
fi
|
|
else
|
|
# Use default CONFIG_FILE_NAME in PROJECT_ROOT
|
|
CONFIG_FILE="$PROJECT_ROOT/$CONFIG_FILE_NAME"
|
|
fi
|
|
|
|
_log INFO "Using Project Root: $PROJECT_ROOT"
|
|
_log INFO "Effective Configuration File: $CONFIG_FILE"
|
|
|
|
_check_dependencies
|
|
# set -x # Enable command tracing (use external zsh -x if needed)
|
|
_load_config
|
|
|
|
# --- Theme Configuration ---
|
|
_log DEBUG "Processing theme configuration..."
|
|
local theme_name="${QSG_CONFIG[site_theme]:-}"
|
|
QSG_CONFIG[theme_static_source_dir]="" # Initialize. Source for theme's static files.
|
|
# QSG_CONFIG[paths_layouts_dir] is loaded from site.conf. It will be overridden if theme provides layouts.
|
|
# QSG_CONFIG[paths_static_dir] is loaded from site.conf (e.g. "static"). This is the root static dir.
|
|
|
|
if [[ -n "$theme_name" ]]; then
|
|
_log INFO "Site theme specified: '$theme_name'"
|
|
local theme_base_path="$PROJECT_ROOT/themes/$theme_name"
|
|
|
|
if [[ ! -d "$theme_base_path" ]]; then
|
|
_log WARNING "Theme directory '$theme_base_path' not found for theme '$theme_name'. Theme will not be applied."
|
|
else
|
|
_log DEBUG "Theme directory found: '$theme_base_path'"
|
|
|
|
# 1. Theme Layouts: Override paths_layouts_dir if theme provides layouts
|
|
local theme_layouts_path="$theme_base_path/layouts"
|
|
if [[ -d "$theme_layouts_path" ]]; then
|
|
_log INFO "Using layouts from theme '$theme_name': $theme_layouts_path"
|
|
QSG_CONFIG[paths_layouts_dir]="$theme_layouts_path" # This now points to theme layouts
|
|
else
|
|
_log DEBUG "No 'layouts' directory in theme '$theme_name'. Using layouts from '${QSG_CONFIG[paths_layouts_dir]}'."
|
|
fi
|
|
|
|
# 2. Theme Static Files Source: Set theme_static_source_dir
|
|
local theme_static_standard_path="$theme_base_path/static"
|
|
if [[ -d "$theme_static_standard_path" ]]; then
|
|
_log INFO "Theme '$theme_name' provides static files from standard path: $theme_static_standard_path"
|
|
QSG_CONFIG[theme_static_source_dir]="$theme_static_standard_path"
|
|
elif [[ -d "$theme_base_path" ]]; then
|
|
# If themes/theme_name/static doesn't exist,
|
|
# check if themes/theme_name itself can serve as a base for static assets (e.g. themes/theme_name/css)
|
|
_log INFO "Theme '$theme_name' provides static files from theme base path: $theme_base_path"
|
|
QSG_CONFIG[theme_static_source_dir]="$theme_base_path"
|
|
else
|
|
_log DEBUG "No 'static' directory found at '$theme_static_standard_path' and theme base path '$theme_base_path' is not a valid fallback."
|
|
# QSG_CONFIG[theme_static_source_dir] remains as initialized (empty)
|
|
fi
|
|
fi
|
|
else
|
|
_log DEBUG "No site_theme specified. Using default project paths for layouts and static files."
|
|
fi
|
|
|
|
|
|
# For debugging: print loaded config
|
|
# for key val in "${(@kv)QSG_CONFIG}"; do
|
|
# _log DEBUG "Config: $key = $val"
|
|
# done
|
|
|
|
_clean_output_dir
|
|
if [[ $? -ne 0 ]]; then _log ERROR "Cleaning output directory failed."; exit 1; fi
|
|
|
|
_copy_static_files
|
|
if [[ $? -ne 0 ]]; then _log ERROR "Copying static files failed."; exit 1; fi
|
|
|
|
# --- Determine Final CSS path for Pandoc --- (Moved here to check after files are copied)
|
|
QSG_CONFIG[pandoc_css_path_arg]=""
|
|
local site_css_file_spec="${QSG_CONFIG[site_theme_css_file]:-}"
|
|
|
|
if [[ -n "$site_css_file_spec" ]]; then
|
|
# site_css_file_spec is the name/path of the CSS file as specified in site.conf,
|
|
# e.g., "minimaltemplate-v1.css" or "css/custom.css".
|
|
# This path is relative to what _copy_static_files copies into output/static/.
|
|
local css_path_in_output_static="$site_css_file_spec"
|
|
local output_dir_from_config_main="${QSG_CONFIG[paths_output_dir]:-output}" # Default to "output" if not set in main
|
|
local base_output_dir_abs_main=""
|
|
if [[ "$output_dir_from_config_main" == /* ]]; then
|
|
base_output_dir_abs_main="$output_dir_from_config_main"
|
|
else
|
|
base_output_dir_abs_main="$PROJECT_ROOT/$output_dir_from_config_main"
|
|
fi
|
|
local expected_css_file_abs_path="$base_output_dir_abs_main/static/$css_path_in_output_static"
|
|
|
|
if [[ -f "$expected_css_file_abs_path" ]]; then
|
|
QSG_CONFIG[pandoc_css_path_arg]="/static/$css_path_in_output_static"
|
|
_log DEBUG "Pandoc CSS path set to: ${QSG_CONFIG[pandoc_css_path_arg]} (file exists: $expected_css_file_abs_path)"
|
|
else
|
|
_log WARNING "Specified 'site_theme_css_file' ('$site_css_file_spec') not found at '$expected_css_file_abs_path' after copying static files. CSS may not be applied via this setting."
|
|
fi
|
|
fi
|
|
|
|
# Fallback to old site_theme_css_path if pandoc_css_path_arg is still empty
|
|
if [[ -z "${QSG_CONFIG[pandoc_css_path_arg]}" ]]; then
|
|
local old_direct_css_path="${QSG_CONFIG[site_theme_css_path]:-}"
|
|
if [[ -n "$old_direct_css_path" ]]; then
|
|
if [[ "$old_direct_css_path" == static/* || "$old_direct_css_path" == /static/* ]]; then
|
|
QSG_CONFIG[pandoc_css_path_arg]="$(echo "$old_direct_css_path" | sed 's|^/*static|/static|')"
|
|
else
|
|
QSG_CONFIG[pandoc_css_path_arg]="/$old_direct_css_path"
|
|
fi
|
|
_log DEBUG "Using fallback 'site_theme_css_path' for Pandoc CSS argument: '${QSG_CONFIG[pandoc_css_path_arg]}'"
|
|
_log WARNING "Consider migrating to 'site_theme_css_file' in site.conf for clearer CSS management."
|
|
elif [[ -n "$site_css_file_spec" ]]; then
|
|
# This case: site_theme_css_file was specified, but the file was not found, and no old_direct_css_path fallback.
|
|
_log DEBUG "Specified 'site_theme_css_file' ('$site_css_file_spec') was not found, and no fallback 'site_theme_css_path' provided. No --css flag for Pandoc."
|
|
else
|
|
# This case: neither site_theme_css_file nor old_direct_css_path were specified.
|
|
_log DEBUG "No CSS file specified via 'site_theme_css_file' or 'site_theme_css_path'. No --css flag for Pandoc."
|
|
fi
|
|
fi
|
|
|
|
_process_markdown_files
|
|
if [[ $? -ne 0 ]]; then _log ERROR "Processing Markdown files failed."; exit 1; fi
|
|
|
|
_generate_index_page
|
|
if [[ $? -ne 0 ]]; then _log ERROR "Index page generation failed."; exit 1; fi
|
|
|
|
_generate_rss_feed
|
|
if [[ $? -ne 0 ]]; then _log ERROR "RSS feed generation failed."; exit 1; fi
|
|
|
|
_log INFO "Final state of output directory (${QSG_CONFIG[paths_output_dir]}):
|
|
$(ls -R "${QSG_CONFIG[paths_output_dir]}" 2>&1)"
|
|
_log SUCCESS "Site generation complete! Output: ${QSG_CONFIG[paths_output_dir]}"
|
|
# set +x # Disable command tracing
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|