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