451 lines
No EOL
17 KiB
Text
Executable file
451 lines
No EOL
17 KiB
Text
Executable file
#!/usr/bin/env nu
|
|
|
|
# Unified Content Management Script
|
|
# Nushell version of content-manager.sh
|
|
# Replaces: generate-index.sh, validate-id-consistency.sh, validate-content.sh
|
|
|
|
def main [command?: string, ...args] {
|
|
print $"(ansi blue)📝 Unified Content Management Tool(ansi reset)"
|
|
|
|
# Load configuration
|
|
let config = load_content_config
|
|
|
|
if ($command | is-empty) {
|
|
show_content_usage
|
|
exit 0
|
|
}
|
|
|
|
match $command {
|
|
"generate-indices" | "indices" => { cmd_generate_indices $config }
|
|
"validate-ids" | "ids" => { cmd_validate_ids $config }
|
|
"validate-content" | "validate" => { cmd_validate_content $config }
|
|
"validate-consistency" | "consistency" => { cmd_validate_consistency $config }
|
|
"show-stats" | "stats" => { cmd_show_stats $config }
|
|
"help" | "-h" | "--help" => { show_content_usage }
|
|
_ => {
|
|
print $"(ansi red)❌ Unknown command: ($command)(ansi reset)"
|
|
show_content_usage
|
|
exit 1
|
|
}
|
|
}
|
|
}
|
|
|
|
# Load content configuration from environment and files
|
|
def load_content_config [] {
|
|
let content_dir = ($env.SITE_CONTENT_PATH? | default "site/content")
|
|
let languages = ["en", "es"]
|
|
|
|
{
|
|
content_dir: $content_dir,
|
|
languages: $languages,
|
|
content_types: (get_enabled_content_types $content_dir),
|
|
templates_dir: "scripts/content/templates"
|
|
}
|
|
}
|
|
|
|
# Show usage information
|
|
def show_content_usage [] {
|
|
print "Usage: nu content-manager.nu [COMMAND] [OPTIONS]"
|
|
print ""
|
|
print "Commands:"
|
|
print " generate-indices Generate JSON indices from markdown frontmatter"
|
|
print " validate-ids Validate ID consistency across languages"
|
|
print " validate-content Validate content structure and fields"
|
|
print " validate-consistency Validate content consistency across languages"
|
|
print " show-stats Show content statistics"
|
|
print " help Show this help message"
|
|
print ""
|
|
print "Examples:"
|
|
print " nu content-manager.nu generate-indices"
|
|
print " nu content-manager.nu validate-content"
|
|
print " nu content-manager.nu show-stats"
|
|
}
|
|
|
|
# Get enabled content types from content-kinds.toml
|
|
def get_enabled_content_types [content_dir: string] {
|
|
let content_kinds_file = $"($content_dir)/content-kinds.toml"
|
|
|
|
if not ($content_kinds_file | path exists) {
|
|
print $"(ansi yellow)⚠️ Warning: content-kinds.toml not found, using default types(ansi reset)"
|
|
return ["blog", "recipes"]
|
|
}
|
|
|
|
# Parse TOML file for enabled content types (simplified version)
|
|
mut enabled_types = []
|
|
let content = (open $content_kinds_file | lines)
|
|
|
|
mut in_content_kinds_section = false
|
|
mut current_directory = ""
|
|
mut current_enabled = false
|
|
|
|
for line in $content {
|
|
if ($line | str starts-with "#") or ($line | str trim | is-empty) {
|
|
continue
|
|
}
|
|
|
|
if $line =~ "^\\[\\[content_kinds\\]\\]" {
|
|
# Process previous section if enabled
|
|
if $in_content_kinds_section and $current_enabled and ($current_directory | is-not-empty) {
|
|
let dir_path = $"($content_dir)/($current_directory)"
|
|
if ($dir_path | path exists) {
|
|
$enabled_types = ($enabled_types | append $current_directory)
|
|
}
|
|
}
|
|
|
|
$in_content_kinds_section = true
|
|
$current_directory = ""
|
|
$current_enabled = false
|
|
continue
|
|
}
|
|
|
|
if $in_content_kinds_section {
|
|
if $line =~ "^\\[\\[" {
|
|
# Process current section before moving to next
|
|
if $current_enabled and ($current_directory | is-not-empty) {
|
|
let dir_path = $"($content_dir)/($current_directory)"
|
|
if ($dir_path | path exists) {
|
|
$enabled_types = ($enabled_types | append $current_directory)
|
|
}
|
|
}
|
|
$in_content_kinds_section = false
|
|
continue
|
|
}
|
|
|
|
if $line =~ 'directory\\s*=\\s*"([^"]+)"' {
|
|
let matches = ($line | parse --regex 'directory\\s*=\\s*"([^"]+)"')
|
|
if ($matches | length) > 0 {
|
|
$current_directory = ($matches | first | get capture0)
|
|
}
|
|
} else if $line =~ 'enabled\\s*=\\s*true' {
|
|
$current_enabled = true
|
|
}
|
|
}
|
|
}
|
|
|
|
# Process final section
|
|
if $in_content_kinds_section and $current_enabled and ($current_directory | is-not-empty) {
|
|
let dir_path = $"($content_dir)/($current_directory)"
|
|
if ($dir_path | path exists) {
|
|
$enabled_types = ($enabled_types | append $current_directory)
|
|
}
|
|
}
|
|
|
|
if ($enabled_types | is-empty) {
|
|
return ["blog", "recipes"]
|
|
}
|
|
|
|
$enabled_types
|
|
}
|
|
|
|
# Generate JSON indices from markdown frontmatter
|
|
def cmd_generate_indices [config] {
|
|
print $"(ansi blue)🔧 Generating content indices...(ansi reset)"
|
|
|
|
mut total_errors = 0
|
|
|
|
for content_type in $config.content_types {
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
|
|
if not ($content_dir | path exists) {
|
|
print $"(ansi yellow)⚠️ Content directory not found: ($content_dir)(ansi reset)"
|
|
continue
|
|
}
|
|
|
|
print $"(ansi yellow)🔧 Generating ($content_type) index for language: ($language)(ansi reset)"
|
|
|
|
let index_file = $"($content_dir)/index.json"
|
|
mut entries = []
|
|
|
|
# Process each markdown file
|
|
let md_files = (ls $"($content_dir)/**/*.md" | where type == file)
|
|
for file_info in $md_files {
|
|
let md_file = $file_info.name
|
|
try {
|
|
let entry = (process_markdown_file $md_file $content_type $language)
|
|
$entries = ($entries | append $entry)
|
|
} catch {
|
|
print $"(ansi yellow)⚠️ Failed to process: ($md_file)(ansi reset)"
|
|
}
|
|
}
|
|
|
|
# Determine array name (keep legacy naming for recipes)
|
|
let array_name = if $content_type == "recipes" { "prescriptions" } else { $"($content_type)_posts" }
|
|
|
|
# Build final index.json
|
|
let index_content = {
|
|
($array_name): $entries,
|
|
language: $language
|
|
}
|
|
|
|
try {
|
|
$index_content | to json | save $index_file
|
|
print $"(ansi green)✅ Generated valid ($content_type) index (($language)): ($entries | length) entries(ansi reset)"
|
|
} catch {
|
|
print $"(ansi red)❌ Failed to generate JSON for ($content_type) (($language))(ansi reset)"
|
|
$total_errors = ($total_errors + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if $total_errors == 0 {
|
|
print $"(ansi green)🎉 Index generation completed successfully!(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Index generation completed with ($total_errors) errors(ansi reset)"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Process a single markdown file and extract metadata
|
|
def process_markdown_file [md_file: string, content_type: string, language: string] {
|
|
let content = (open $md_file | lines)
|
|
let filename = ($md_file | path basename | path parse | get stem)
|
|
|
|
# Extract frontmatter
|
|
let frontmatter = extract_frontmatter_from_lines $content
|
|
|
|
# Extract basic fields with fallbacks
|
|
let title = (extract_frontmatter_field $frontmatter "title" $filename)
|
|
let description = (extract_frontmatter_field $frontmatter "description" "")
|
|
let author = (extract_frontmatter_field $frontmatter "author" "")
|
|
let date = (extract_frontmatter_field $frontmatter "date" "")
|
|
let category = (extract_frontmatter_field $frontmatter "category" "")
|
|
let tags = (extract_frontmatter_array $frontmatter "tags")
|
|
let published = (extract_frontmatter_field $frontmatter "published" "true")
|
|
|
|
# Only include published content
|
|
if $published != "true" {
|
|
return null
|
|
}
|
|
|
|
# Create content entry
|
|
{
|
|
id: $filename,
|
|
title: $title,
|
|
description: $description,
|
|
author: $author,
|
|
date: $date,
|
|
category: $category,
|
|
tags: $tags,
|
|
filename: $filename,
|
|
html_file: $"($filename).html"
|
|
}
|
|
}
|
|
|
|
# Extract frontmatter from content lines
|
|
def extract_frontmatter_from_lines [content: list<string>] {
|
|
if ($content | length) == 0 or ($content | first) != "---" {
|
|
return []
|
|
}
|
|
|
|
mut frontmatter_lines = []
|
|
mut in_frontmatter = false
|
|
mut line_count = 0
|
|
|
|
for line in $content {
|
|
$line_count = ($line_count + 1)
|
|
if $line_count == 1 and $line == "---" {
|
|
$in_frontmatter = true
|
|
continue
|
|
}
|
|
if $in_frontmatter and $line == "---" {
|
|
break
|
|
}
|
|
if $in_frontmatter {
|
|
$frontmatter_lines = ($frontmatter_lines | append $line)
|
|
}
|
|
}
|
|
|
|
$frontmatter_lines
|
|
}
|
|
|
|
# Extract field value from frontmatter
|
|
def extract_frontmatter_field [frontmatter: list<string>, key: string, default: string] {
|
|
for line in $frontmatter {
|
|
if $line =~ $"($key):\\s*(.+)" {
|
|
let matches = ($line | parse --regex $"($key):\\s*\"?([^\"\\n]+)\"?")
|
|
if ($matches | length) > 0 {
|
|
return ($matches | first | get capture0 | str trim)
|
|
}
|
|
}
|
|
}
|
|
$default
|
|
}
|
|
|
|
# Extract array values from frontmatter
|
|
def extract_frontmatter_array [frontmatter: list<string>, key: string] {
|
|
for line in $frontmatter {
|
|
if $line =~ $"($key):\\s*\\[(.+)\\]" {
|
|
let matches = ($line | parse --regex $"($key):\\s*\\[(.+)\\]")
|
|
if ($matches | length) > 0 {
|
|
let array_content = ($matches | first | get capture0)
|
|
return ($array_content | split row "," | each { |item| $item | str trim | str replace -a '"' "" })
|
|
}
|
|
}
|
|
}
|
|
[]
|
|
}
|
|
|
|
# Validate ID consistency across languages
|
|
def cmd_validate_ids [config] {
|
|
print $"(ansi blue)🔧 Validating ID consistency across languages...(ansi reset)"
|
|
|
|
mut total_errors = 0
|
|
|
|
for content_type in $config.content_types {
|
|
print $"(ansi yellow)Validating ($content_type) IDs...(ansi reset)"
|
|
|
|
# Get IDs for each language
|
|
mut language_ids = {}
|
|
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
if ($content_dir | path exists) {
|
|
let md_files = (ls $"($content_dir)/**/*.md" | where type == file)
|
|
let ids = ($md_files | each { |file| $file.name | path basename | path parse | get stem })
|
|
$language_ids = ($language_ids | upsert $language $ids)
|
|
}
|
|
}
|
|
|
|
# Compare IDs across languages
|
|
let languages_with_content = ($language_ids | columns)
|
|
if ($languages_with_content | length) >= 2 {
|
|
let primary_lang = ($languages_with_content | first)
|
|
let primary_ids = ($language_ids | get $primary_lang)
|
|
|
|
for lang in ($languages_with_content | skip 1) {
|
|
let lang_ids = ($language_ids | get $lang)
|
|
|
|
# Find missing IDs
|
|
let missing_in_lang = ($primary_ids | where {|id| $id not-in $lang_ids})
|
|
let missing_in_primary = ($lang_ids | where {|id| $id not-in $primary_ids})
|
|
|
|
if ($missing_in_lang | length) > 0 {
|
|
print $"(ansi red)❌ ($content_type): Missing in ($lang): ($missing_in_lang | str join ', ')(ansi reset)"
|
|
$total_errors = ($total_errors + ($missing_in_lang | length))
|
|
}
|
|
|
|
if ($missing_in_primary | length) > 0 {
|
|
print $"(ansi red)❌ ($content_type): Missing in ($primary_lang): ($missing_in_primary | str join ', ')(ansi reset)"
|
|
$total_errors = ($total_errors + ($missing_in_primary | length))
|
|
}
|
|
}
|
|
|
|
if ($missing_in_lang | length) == 0 and ($missing_in_primary | length) == 0 {
|
|
print $"(ansi green)✅ ($content_type): ID consistency validated across languages(ansi reset)"
|
|
}
|
|
}
|
|
}
|
|
|
|
if $total_errors == 0 {
|
|
print $"(ansi green)🎉 ID validation completed successfully!(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ ID validation completed with ($total_errors) inconsistencies(ansi reset)"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Validate content structure and required fields
|
|
def cmd_validate_content [config] {
|
|
print $"(ansi blue)🔧 Validating content structure and fields...(ansi reset)"
|
|
|
|
mut total_errors = 0
|
|
|
|
for content_type in $config.content_types {
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
|
|
if not ($content_dir | path exists) {
|
|
continue
|
|
}
|
|
|
|
print $"(ansi yellow)Validating ($content_type) content (($language))...(ansi reset)"
|
|
|
|
let md_files = (ls $"($content_dir)/**/*.md" | where type == file)
|
|
for file_info in $md_files {
|
|
let md_file = $file_info.name
|
|
let filename = ($md_file | path basename)
|
|
|
|
try {
|
|
let content = (open $md_file | lines)
|
|
let frontmatter = (extract_frontmatter_from_lines $content)
|
|
|
|
# Validate required fields
|
|
let title = (extract_frontmatter_field $frontmatter "title" "")
|
|
if ($title | is-empty) {
|
|
print $"(ansi red)❌ ($filename): Missing required field 'title'(ansi reset)"
|
|
$total_errors = ($total_errors + 1)
|
|
}
|
|
|
|
# Validate content has body (more than just frontmatter)
|
|
let content_lines = ($content | length)
|
|
let frontmatter_lines = ($frontmatter | length)
|
|
if ($content_lines - $frontmatter_lines - 2) < 3 { # Account for --- delimiters
|
|
print $"(ansi yellow)⚠️ ($filename): Very short content (may be empty)(ansi reset)"
|
|
}
|
|
|
|
} catch {
|
|
print $"(ansi red)❌ ($filename): Failed to parse content(ansi reset)"
|
|
$total_errors = ($total_errors + 1)
|
|
}
|
|
}
|
|
|
|
# Validate index.json exists and is valid
|
|
let index_file = $"($content_dir)/index.json"
|
|
if ($index_file | path exists) {
|
|
try {
|
|
open $index_file | from json | ignore
|
|
print $"(ansi green)✅ ($content_type) (($language)): Valid index.json(ansi reset)"
|
|
} catch {
|
|
print $"(ansi red)❌ ($content_type) (($language)): Invalid index.json(ansi reset)"
|
|
$total_errors = ($total_errors + 1)
|
|
}
|
|
} else {
|
|
print $"(ansi yellow)⚠️ ($content_type) (($language)): Missing index.json(ansi reset)"
|
|
}
|
|
}
|
|
}
|
|
|
|
if $total_errors == 0 {
|
|
print $"(ansi green)🎉 Content validation completed successfully!(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Content validation completed with ($total_errors) errors(ansi reset)"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Validate content consistency across languages
|
|
def cmd_validate_consistency [config] {
|
|
print $"(ansi blue)🔧 Validating content consistency across languages...(ansi reset)"
|
|
|
|
cmd_validate_ids $config
|
|
cmd_validate_content $config
|
|
}
|
|
|
|
# Show content statistics
|
|
def cmd_show_stats [config] {
|
|
print $"(ansi blue)📊 Content Statistics(ansi reset)"
|
|
print $"(ansi blue)Content root: ($config.content_dir)(ansi reset)"
|
|
print $"(ansi blue)Languages: ($config.languages | str join ', ')(ansi reset)"
|
|
print $"(ansi blue)Content types: ($config.content_types | str join ', ')(ansi reset)"
|
|
print ""
|
|
|
|
for content_type in $config.content_types {
|
|
print $"(ansi cyan)📝 ($content_type | str title-case):(ansi reset)"
|
|
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
|
|
if ($content_dir | path exists) {
|
|
let md_count = (ls $"($content_dir)/**/*.md" | where type == file | length)
|
|
let index_exists = ($"($content_dir)/index.json" | path exists)
|
|
let index_status = if $index_exists { "✅" } else { "❌" }
|
|
|
|
print $" ($language): ($md_count) files, index: ($index_status)"
|
|
} else {
|
|
print $" ($language): directory not found"
|
|
}
|
|
}
|
|
print ""
|
|
}
|
|
} |