396 lines
No EOL
13 KiB
Bash
Executable file
396 lines
No EOL
13 KiB
Bash
Executable file
#!/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 |