website-htmx-rustelo-code/scripts/sh/content/lib.sh
2026-07-10 03:44:13 +01:00

396 lines
No EOL
13 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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