website-htmx-rustelo-code/scripts/sh/content/lib.sh

396 lines
13 KiB
Bash
Raw Permalink Normal View History

2026-07-10 03:44:13 +01:00
#!/bin/bash
# Content Management Shared Library
# This library provides common functions used across content scripts
set -e
# Configuration defaults
CONTENT_DIR="${SITE_CONTENT_PATH:-site/content}"
CONTENT_KINDS_FILE="${CONTENT_DIR}/content-kinds.toml"
DEFAULT_LANGUAGES=("en" "es")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# =============================================================================
# LOGGING FUNCTIONS
# =============================================================================
log_error() {
echo -e "${RED}❌ ERROR: $1${NC}" >&2
}
log_warning() {
echo -e "${YELLOW}⚠️ WARNING: $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_info() {
echo -e "${BLUE} $1${NC}"
}
log_step() {
echo -e "${CYAN}🔧 $1${NC}"
}
# =============================================================================
# CONTENT-KINDS.TOML PARSING
# =============================================================================
# Get enabled content types from content-kinds.toml
get_enabled_content_types() {
if [ ! -f "$CONTENT_KINDS_FILE" ]; then
log_warning "content-kinds.toml not found at $CONTENT_KINDS_FILE, using defaults"
echo "blog recipes"
return
fi
# Robust TOML parsing - process line by line and track state
local in_content_kinds_section=false
local current_name=""
local current_directory=""
local current_enabled=""
local enabled_types=()
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "${line// }" ]] && continue
# Check for new content_kinds section
if [[ "$line" =~ ^\[\[content_kinds\]\] ]]; then
# Process previous section if we have one
if [[ "$in_content_kinds_section" == true && "$current_enabled" == "true" && -n "$current_directory" ]]; then
if [ -d "${CONTENT_DIR}/${current_directory}" ]; then
enabled_types+=("$current_directory")
log_info "Content type '$current_name' -> directory '${current_directory}' enabled" >&2
else
log_warning "Content type '$current_name' enabled but directory '${CONTENT_DIR}/${current_directory}' missing - SKIPPING" >&2
fi
fi
# Reset for new section
in_content_kinds_section=true
current_name=""
current_directory=""
current_enabled=""
continue
fi
# If we're in a content_kinds section, parse the fields
if [[ "$in_content_kinds_section" == true ]]; then
# Check for another section starting (end current one)
if [[ "$line" =~ ^\[\[ ]]; then
# Process current section before moving to next
if [[ "$current_enabled" == "true" && -n "$current_directory" ]]; then
if [ -d "${CONTENT_DIR}/${current_directory}" ]; then
enabled_types+=("$current_directory")
log_info "Content type '$current_name' -> directory '${current_directory}' enabled" >&2
else
log_warning "Content type '$current_name' enabled but directory '${CONTENT_DIR}/${current_directory}' missing - SKIPPING" >&2
fi
fi
in_content_kinds_section=false
continue
fi
# Extract fields
if [[ "$line" =~ ^name[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
current_name="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^directory[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
current_directory="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^enabled[[:space:]]*=[[:space:]]*true ]]; then
current_enabled="true"
fi
fi
done < "$CONTENT_KINDS_FILE"
# Don't forget to process the last section
if [[ "$in_content_kinds_section" == true && "$current_enabled" == "true" && -n "$current_directory" ]]; then
if [ -d "${CONTENT_DIR}/${current_directory}" ]; then
enabled_types+=("$current_directory")
log_info "Content type '$current_name' -> directory '${current_directory}' enabled" >&2
else
log_warning "Content type '$current_name' enabled but directory '${CONTENT_DIR}/${current_directory}' missing - SKIPPING" >&2
fi
fi
if [ ${#enabled_types[@]} -eq 0 ]; then
log_warning "No enabled content types found, discovering from directory structure" >&2
# Fallback: discover from directory structure
local discovered_types=()
for dir in "${CONTENT_DIR}"/*; do
if [ -d "$dir" ]; then
local dirname=$(basename "$dir")
# Skip non-content directories
if [[ "$dirname" != "locales" && "$dirname" != "themes" && "$dirname" != "menus" && "$dirname" != "footer" && "$dirname" != "tmp" && "$dirname" != "routes" && "$dirname" != "pages" ]]; then
discovered_types+=("$dirname")
fi
fi
done
if [ ${#discovered_types[@]} -eq 0 ]; then
echo "blog recipes"
else
echo "${discovered_types[@]}"
fi
else
echo "${enabled_types[@]}"
fi
}
# Get supported languages from directory structure
get_supported_languages() {
local content_types=($(get_enabled_content_types))
local languages=()
for content_type in "${content_types[@]}"; do
local content_dir="${CONTENT_DIR}/${content_type}"
if [ -d "$content_dir" ]; then
for lang_dir in "$content_dir"/*/; do
if [ -d "$lang_dir" ]; then
local lang=$(basename "$lang_dir")
if [[ ! " ${languages[@]} " =~ " ${lang} " ]]; then
languages+=("$lang")
fi
fi
done
fi
done
if [ ${#languages[@]} -eq 0 ]; then
echo "${DEFAULT_LANGUAGES[@]}"
else
echo "${languages[@]}"
fi
}
# =============================================================================
# FRONTMATTER PROCESSING
# =============================================================================
# Extract frontmatter from markdown file
extract_frontmatter() {
local file="$1"
if [ ! -f "$file" ]; then
return 1
fi
# Check if file starts with frontmatter
if ! head -1 "$file" | grep -q "^---$"; then
return 1
fi
# Extract YAML frontmatter between --- delimiters
awk '/^---$/{flag++; next} flag==1 && /^---$/{flag++; exit} flag==1{print}' "$file"
}
# Extract specific field from frontmatter
extract_field() {
local frontmatter="$1"
local field="$2"
local default_value="$3"
# Handle different field types
case "$field" in
"tags"|"translations")
# Extract array values and format as JSON
local value=$(echo "$frontmatter" | grep -E "^${field}:" | sed 's/.*: *\[//' | sed 's/\]//' | tr -d '"' | sed 's/,/, /g')
if [ -n "$value" ]; then
# Convert to proper JSON array format - split by comma and quote each item
echo "[$value]" | sed 's/\([^,]*\),\s*/"\1",/g' | sed 's/\([^,]*\)$/"\1"/' | sed 's/^\[,/[/' | sed 's/,"]/"]/'
else
echo "[]"
fi
;;
"featured"|"published")
# Boolean fields
local value=$(echo "$frontmatter" | grep -E "^${field}:" | sed 's/.*: *//' | tr '[:upper:]' '[:lower:]')
case "$value" in
true|yes|1) echo "true" ;;
false|no|0) echo "false" ;;
*) echo "${default_value:-false}" ;;
esac
;;
*)
# String fields
local value=$(echo "$frontmatter" | grep -E "^${field}:" | sed 's/.*: *//' | sed 's/^"*//' | sed 's/"*$//')
if [ -n "$value" ]; then
echo "$value"
else
echo "${default_value:-}"
fi
;;
esac
}
# Extract ID from frontmatter or use filename as fallback
extract_id() {
local file="$1"
local frontmatter=$(extract_frontmatter "$file")
if [ -n "$frontmatter" ]; then
local id=$(extract_field "$frontmatter" "id")
if [ -n "$id" ]; then
echo "$id"
return
fi
fi
# Fallback to filename without extension
basename "$file" .md
}
# =============================================================================
# FILE AND DIRECTORY UTILITIES
# =============================================================================
# Get all markdown files for a content type and language
get_markdown_files() {
local content_type="$1"
local language="$2"
local content_dir="${CONTENT_DIR}/${content_type}/${language}"
if [ -d "$content_dir" ]; then
find "$content_dir" -name "*.md" -type f | sort
fi
}
# Check if a content directory exists
content_dir_exists() {
local content_type="$1"
local language="$2"
[ -d "${CONTENT_DIR}/${content_type}/${language}" ]
}
# Create directory if it doesn't exist
ensure_dir() {
local dir="$1"
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
log_info "Created directory: $dir"
fi
}
# =============================================================================
# VALIDATION UTILITIES
# =============================================================================
# Validate JSON file
validate_json() {
local file="$1"
if command -v jq >/dev/null 2>&1; then
if jq empty "$file" 2>/dev/null; then
return 0
else
return 1
fi
else
log_warning "jq not available, skipping JSON validation"
return 0
fi
}
# Check if file has valid frontmatter
has_valid_frontmatter() {
local file="$1"
[ -f "$file" ] && extract_frontmatter "$file" >/dev/null 2>&1
}
# =============================================================================
# CONTENT PROCESSING UTILITIES
# =============================================================================
# Process a single markdown file and extract metadata
process_markdown_file() {
local file="$1"
local content_type="$2"
local language="$3"
if ! has_valid_frontmatter "$file"; then
log_warning "Skipping file without valid frontmatter: $file"
return 1
fi
local filename=$(basename "$file" .md)
local frontmatter=$(extract_frontmatter "$file")
# Extract all standard fields
local id=$(extract_field "$frontmatter" "id" "$filename")
local title=$(extract_field "$frontmatter" "title" "Untitled")
local subtitle=$(extract_field "$frontmatter" "subtitle")
local excerpt=$(extract_field "$frontmatter" "excerpt")
local author=$(extract_field "$frontmatter" "author")
local date=$(extract_field "$frontmatter" "date")
local read_time=$(extract_field "$frontmatter" "read_time")
local category=$(extract_field "$frontmatter" "category")
local difficulty=$(extract_field "$frontmatter" "difficulty")
local prep_time=$(extract_field "$frontmatter" "prep_time")
local tags=$(extract_field "$frontmatter" "tags")
local featured=$(extract_field "$frontmatter" "featured" "false")
local published=$(extract_field "$frontmatter" "published" "true")
local translations=$(extract_field "$frontmatter" "translations")
# Output as JSON object
cat << EOF
{
"id": "$id",
"title": "$title",
"subtitle": ${subtitle:+\"$subtitle\"},
"excerpt": ${excerpt:+\"$excerpt\"},
"author": ${author:+\"$author\"},
"date": ${date:+\"$date\"},
"read_time": ${read_time:+\"$read_time\"},
"category": ${category:+\"$category\"},
"difficulty": ${difficulty:+\"$difficulty\"},
"prep_time": ${prep_time:+\"$prep_time\"},
"tags": $tags,
"featured": $featured,
"published": $published,
"language": "$language",
"source_file": "${CONTENT_DIR}/$content_type/$language/$filename.md",
"html_file": "/$content_type/$language/$filename.html",
"translations": $translations
}
EOF
}
# =============================================================================
# INITIALIZATION
# =============================================================================
# Initialize library (call this from scripts that source this library)
init_content_lib() {
local script_name="$1"
log_info "Initializing content library for: $script_name"
log_info "Content directory: $CONTENT_DIR"
if [ ! -d "$CONTENT_DIR" ]; then
log_error "Content directory not found: $CONTENT_DIR"
exit 1
fi
local content_types=($(get_enabled_content_types))
local languages=($(get_supported_languages))
log_info "Enabled content types: ${content_types[*]}"
log_info "Supported languages: ${languages[*]}"
}
# Export functions that may be used by other scripts
export -f log_error log_warning log_success log_info log_step
export -f get_enabled_content_types get_supported_languages
export -f extract_frontmatter extract_field extract_id
export -f get_markdown_files content_dir_exists ensure_dir
export -f validate_json has_valid_frontmatter process_markdown_file
export -f init_content_lib