449 lines
No EOL
17 KiB
Text
Executable file
449 lines
No EOL
17 KiB
Text
Executable file
#!/usr/bin/env nu
|
|
|
|
# Content Validation Script
|
|
# Nushell version of validate-content.sh
|
|
# Validates JSON structure, required fields, and content integrity
|
|
|
|
def main [...args] {
|
|
let config = {
|
|
content_dir: ($env.SITE_CONTENT_PATH? | default "site/content"),
|
|
languages: ["en", "es"]
|
|
}
|
|
|
|
print $"(ansi blue)🔍 Content Validation Tool(ansi reset)"
|
|
|
|
# Get content types dynamically
|
|
let content_types = get_content_types $config.content_dir
|
|
|
|
if ($args | length) > 0 and ($args | first) in ["-h", "--help", "help"] {
|
|
show_validation_usage
|
|
exit 0
|
|
}
|
|
|
|
# Run comprehensive validation
|
|
mut total_errors = 0
|
|
|
|
$total_errors = $total_errors + (validate_directory_structure $config $content_types)
|
|
$total_errors = $total_errors + (validate_json_files $config $content_types)
|
|
$total_errors = $total_errors + (validate_content_fields $config $content_types)
|
|
$total_errors = $total_errors + (validate_cross_references $config $content_types)
|
|
|
|
show_validation_summary $config $content_types $total_errors
|
|
|
|
if $total_errors > 0 {
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Show usage information
|
|
def show_validation_usage [] {
|
|
print "Usage: nu validate-content.nu [OPTIONS]"
|
|
print ""
|
|
print "This script validates all localized content for:"
|
|
print " • Directory structure consistency"
|
|
print " • JSON file validity"
|
|
print " • Required content fields"
|
|
print " • Cross-reference integrity"
|
|
print ""
|
|
print "Options:"
|
|
print " -h, --help Show this help message"
|
|
print ""
|
|
print "Environment Variables:"
|
|
print " SITE_CONTENT_PATH Content root directory [default: site/content]"
|
|
}
|
|
|
|
# Get content types from directory or content-kinds.toml
|
|
def get_content_types [content_dir: string] {
|
|
let content_kinds_file = $"($content_dir)/content-kinds.toml"
|
|
|
|
if ($content_kinds_file | path exists) {
|
|
# Extract from TOML file
|
|
try {
|
|
let content = (open $content_kinds_file)
|
|
# Simple extraction for now - can be enhanced with proper TOML parsing
|
|
$content | lines
|
|
| where {|line| $line =~ "directory\\s*=" }
|
|
| each {|line|
|
|
let matches = ($line | parse --regex 'directory\\s*=\\s*"([^"]+)"')
|
|
if ($matches | length) > 0 {
|
|
$matches | first | get capture0
|
|
} else { null }
|
|
}
|
|
| where {|x| $x != null}
|
|
} catch {
|
|
# Fallback to directory discovery
|
|
ls $content_dir
|
|
| where type == dir
|
|
| where name !~ "(locales|themes|menus|footer|tmp|routes)"
|
|
| get name
|
|
| path basename
|
|
}
|
|
} else {
|
|
# Discover from directory structure
|
|
ls $content_dir
|
|
| where type == dir
|
|
| where name !~ "(locales|themes|menus|footer|tmp|routes)"
|
|
| get name
|
|
| path basename
|
|
}
|
|
}
|
|
|
|
# Validate directory structure consistency
|
|
def validate_directory_structure [config, content_types: list<string>] {
|
|
print $"(ansi blue)🔧 Validating directory structure...(ansi reset)"
|
|
|
|
mut errors = 0
|
|
|
|
# Check if content root exists
|
|
if not ($config.content_dir | path exists) {
|
|
print $"(ansi red)❌ Content root directory not found: ($config.content_dir)(ansi reset)"
|
|
return 1
|
|
}
|
|
|
|
# Validate each content type has language subdirectories
|
|
for content_type in $content_types {
|
|
let content_type_dir = $"($config.content_dir)/($content_type)"
|
|
|
|
if not ($content_type_dir | path exists) {
|
|
print $"(ansi red)❌ Content type directory not found: ($content_type_dir)(ansi reset)"
|
|
$errors += 1
|
|
continue
|
|
}
|
|
|
|
print $"(ansi yellow) Checking ($content_type) structure...(ansi reset)"
|
|
|
|
# Check each language directory
|
|
for language in $config.languages {
|
|
let lang_dir = $"($content_type_dir)/($language)"
|
|
|
|
if ($lang_dir | path exists) {
|
|
let md_files = (ls $"($lang_dir)/**/*.md" | where type == file | length)
|
|
if $md_files == 0 {
|
|
print $"(ansi yellow)⚠️ No markdown files in ($lang_dir)(ansi reset)"
|
|
} else {
|
|
print $"(ansi green) ✅ ($language): ($md_files) markdown files(ansi reset)"
|
|
}
|
|
} else {
|
|
print $"(ansi yellow)⚠️ Missing language directory: ($lang_dir)(ansi reset)"
|
|
}
|
|
}
|
|
}
|
|
|
|
if $errors == 0 {
|
|
print $"(ansi green)✅ Directory structure validation passed(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Directory structure validation failed with ($errors) errors(ansi reset)"
|
|
}
|
|
|
|
$errors
|
|
}
|
|
|
|
# Validate JSON files
|
|
def validate_json_files [config, content_types: list<string>] {
|
|
print $"(ansi blue)🔧 Validating JSON files...(ansi reset)"
|
|
|
|
mut errors = 0
|
|
|
|
for content_type in $content_types {
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
|
|
if not ($content_dir | path exists) {
|
|
continue
|
|
}
|
|
|
|
# Check index.json
|
|
let index_file = $"($content_dir)/index.json"
|
|
if ($index_file | path exists) {
|
|
try {
|
|
let index_content = (open $index_file | from json)
|
|
|
|
# Validate structure
|
|
let has_language = ($index_content | columns | where $it == "language" | length) > 0
|
|
if not $has_language {
|
|
print $"(ansi red)❌ ($content_type)/($language): index.json missing 'language' field(ansi reset)"
|
|
$errors += 1
|
|
}
|
|
|
|
# Check for content array
|
|
let array_fields = ($index_content | columns | where $it =~ "_posts|prescriptions")
|
|
if ($array_fields | length) == 0 {
|
|
print $"(ansi red)❌ ($content_type)/($language): index.json missing content array(ansi reset)"
|
|
$errors += 1
|
|
} else {
|
|
let array_name = ($array_fields | first)
|
|
let content_array = ($index_content | get $array_name)
|
|
print $"(ansi green) ✅ ($content_type)/($language): valid index.json with ($content_array | length) entries(ansi reset)"
|
|
}
|
|
|
|
} catch {
|
|
print $"(ansi red)❌ ($content_type)/($language): invalid JSON in index.json(ansi reset)"
|
|
# Note: Error count handled at validation level
|
|
}
|
|
} else {
|
|
print $"(ansi yellow)⚠️ ($content_type)/($language): missing index.json(ansi reset)"
|
|
}
|
|
|
|
# Check meta.json if exists
|
|
let meta_file = $"($content_dir)/meta.json"
|
|
if ($meta_file | path exists) {
|
|
try {
|
|
open $meta_file | from json | ignore
|
|
print $"(ansi green) ✅ ($content_type)/($language): valid meta.json(ansi reset)"
|
|
} catch {
|
|
print $"(ansi red)❌ ($content_type)/($language): invalid JSON in meta.json(ansi reset)"
|
|
# Note: Error count handled at validation level
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if $errors == 0 {
|
|
print $"(ansi green)✅ JSON validation passed(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ JSON validation failed with ($errors) errors(ansi reset)"
|
|
}
|
|
|
|
$errors
|
|
}
|
|
|
|
# Validate content fields in markdown files
|
|
def validate_content_fields [config, content_types: list<string>] {
|
|
print $"(ansi blue)🔧 Validating content fields...(ansi reset)"
|
|
|
|
mut errors = 0
|
|
mut total_files = 0
|
|
|
|
for content_type in $content_types {
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
|
|
if not ($content_dir | path exists) {
|
|
continue
|
|
}
|
|
|
|
let md_files = (ls $"($content_dir)/**/*.md" | where type == file)
|
|
$total_files += ($md_files | length)
|
|
|
|
for file_info in $md_files {
|
|
let md_file = $file_info.name
|
|
let filename = ($md_file | path basename)
|
|
|
|
try {
|
|
let content_lines = (open $md_file | lines)
|
|
let validation_result = (validate_markdown_file $content_lines $filename $content_type)
|
|
|
|
if $validation_result.errors > 0 {
|
|
$errors += $validation_result.errors
|
|
print $"(ansi red)❌ ($filename): ($validation_result.messages | str join ', ')(ansi reset)"
|
|
}
|
|
|
|
} catch {
|
|
print $"(ansi red)❌ ($filename): Failed to read or parse file(ansi reset)"
|
|
# Note: Error count handled at file level
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if $errors == 0 {
|
|
print $"(ansi green)✅ Content fields validation passed \(($total_files) files checked\)(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Content fields validation failed with ($errors) errors in ($total_files) files(ansi reset)"
|
|
}
|
|
|
|
$errors
|
|
}
|
|
|
|
# Validate a single markdown file
|
|
def validate_markdown_file [content_lines: list<string>, filename: string, content_type: string] {
|
|
mut errors = 0
|
|
mut messages = []
|
|
|
|
# Check if file has frontmatter
|
|
if ($content_lines | length) == 0 or ($content_lines | first) != "---" {
|
|
$errors = $errors + 1
|
|
$messages = ($messages | append "Missing frontmatter")
|
|
return {errors: $errors, messages: $messages}
|
|
}
|
|
|
|
# Extract frontmatter
|
|
let frontmatter = extract_frontmatter $content_lines
|
|
|
|
# Check required fields
|
|
let required_fields = ["title"]
|
|
for field in $required_fields {
|
|
let field_value = (extract_frontmatter_field $frontmatter $field "")
|
|
if ($field_value | is-empty) {
|
|
$errors += 1
|
|
$messages = ($messages | append $"Missing required field: ($field)")
|
|
}
|
|
}
|
|
|
|
# Check content has body (more than just frontmatter)
|
|
let frontmatter_lines = ($frontmatter | length)
|
|
let total_lines = ($content_lines | length)
|
|
let body_lines = $total_lines - $frontmatter_lines - 2 # Account for --- delimiters
|
|
|
|
if $body_lines < 3 {
|
|
$messages = ($messages | append "Very short content (may be empty)")
|
|
}
|
|
|
|
# Validate specific fields for content type
|
|
if $content_type == "recipes" {
|
|
let difficulty = (extract_frontmatter_field $frontmatter "difficulty" "")
|
|
if not ($difficulty | is-empty) and $difficulty not-in ["Beginner", "Intermediate", "Advanced"] {
|
|
$errors += 1
|
|
$messages = ($messages | append "Invalid difficulty level (must be Beginner, Intermediate, or Advanced)")
|
|
}
|
|
}
|
|
|
|
{errors: $errors, messages: $messages}
|
|
}
|
|
|
|
# Extract frontmatter from content lines
|
|
def extract_frontmatter [content_lines: list<string>] {
|
|
if ($content_lines | length) == 0 or ($content_lines | first) != "---" {
|
|
return []
|
|
}
|
|
|
|
mut frontmatter_lines = []
|
|
mut in_frontmatter = false
|
|
mut line_count = 0
|
|
|
|
for line in $content_lines {
|
|
$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
|
|
}
|
|
|
|
# Validate cross-references between files
|
|
def validate_cross_references [config, content_types: list<string>] {
|
|
print $"(ansi blue)🔧 Validating cross-references...(ansi reset)"
|
|
|
|
mut errors = 0
|
|
|
|
# Check if index.json entries match actual files
|
|
for content_type in $content_types {
|
|
for language in $config.languages {
|
|
let content_dir = $"($config.content_dir)/($content_type)/($language)"
|
|
let index_file = $"($content_dir)/index.json"
|
|
|
|
if not ($content_dir | path exists) or not ($index_file | path exists) {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
let index_content = (open $index_file | from json)
|
|
|
|
# Get content array
|
|
let array_fields = ($index_content | columns | where $it =~ "_posts|prescriptions")
|
|
if ($array_fields | length) == 0 {
|
|
continue
|
|
}
|
|
|
|
let array_name = ($array_fields | first)
|
|
let content_entries = ($index_content | get $array_name)
|
|
|
|
# Get actual markdown files
|
|
let md_files = (ls $"($content_dir)/**/*.md" | where type == file)
|
|
let actual_ids = ($md_files | each { |file| $file.name | path basename | path parse | get stem })
|
|
|
|
# Check index entries exist as files
|
|
for entry in $content_entries {
|
|
if "id" in ($entry | columns) {
|
|
let entry_id = ($entry | get id)
|
|
if $entry_id not-in $actual_ids {
|
|
print $"(ansi red)❌ ($content_type)/($language): index.json references missing file: ($entry_id).md(ansi reset)"
|
|
$errors += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check files exist in index
|
|
for file_id in $actual_ids {
|
|
let index_entries_with_id = ($content_entries | where id == $file_id)
|
|
if ($index_entries_with_id | length) == 0 {
|
|
print $"(ansi yellow)⚠️ ($content_type)/($language): file ($file_id).md not in index.json(ansi reset)"
|
|
}
|
|
}
|
|
|
|
if $errors == 0 {
|
|
print $"(ansi green) ✅ ($content_type)/($language): cross-references validated(ansi reset)"
|
|
}
|
|
|
|
} catch {
|
|
print $"(ansi red)❌ ($content_type)/($language): Failed to validate cross-references(ansi reset)"
|
|
# Note: Error count handled at validation level
|
|
}
|
|
}
|
|
}
|
|
|
|
if $errors == 0 {
|
|
print $"(ansi green)✅ Cross-references validation passed(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Cross-references validation failed with ($errors) errors(ansi reset)"
|
|
}
|
|
|
|
$errors
|
|
}
|
|
|
|
# Show validation summary
|
|
def show_validation_summary [config, content_types: list<string>, total_errors: int] {
|
|
print ""
|
|
print $"(ansi blue)📊 Validation Summary(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: ($content_types | str join ', ')(ansi reset)"
|
|
print ""
|
|
|
|
# Show file counts
|
|
mut total_files = 0
|
|
for content_type in $content_types {
|
|
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)
|
|
$total_files = $total_files + $md_count
|
|
}
|
|
}
|
|
}
|
|
|
|
print $"(ansi blue)Total files validated: ($total_files)(ansi reset)"
|
|
|
|
if $total_errors == 0 {
|
|
print $"(ansi green)🎉 All validations passed successfully!(ansi reset)"
|
|
} else {
|
|
print $"(ansi red)❌ Validation failed with ($total_errors) total errors(ansi reset)"
|
|
print ""
|
|
print $"(ansi yellow)💡 To fix errors:(ansi reset)"
|
|
print " 1. Check missing or invalid frontmatter fields"
|
|
print " 2. Ensure JSON files have valid syntax"
|
|
print " 3. Verify all referenced files exist"
|
|
print " 4. Run content generation to create missing files"
|
|
}
|
|
} |