build.sh (15966B)
1 #!/bin/sh 2 # ============================================================================ 3 # ██╗ ██╗███████╗██████╗ ███████╗ ██████╗ ██████╗ 4 # ██║ ██║██╔════╝██╔══██╗██╔════╝██╔═══██╗ ╚════██╗ 5 # ██║ ██║█████╗ ██████╔╝███████╗██║ ██║ █████╔╝ 6 # ╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║██║ ██║ ██╔═══╝ 7 # ╚████╔╝ ███████╗██║ ██║███████║╚██████╔╝███████╗███████╗ 8 # ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝╚══════╝ 9 # verso_2 - web framework? 10 # ============================================================================ 11 # 12 # Copyright (C) 2026 Binkd. 13 # 14 # This file is part of verso_2. 15 # 16 # verso_2 is free software: you can redistribute it and/or modify it under the 17 # terms of the GNU General Public License as published by the Free Software 18 # Foundation, either version 3 of the License, or (at your option) any later 19 # version. 20 # 21 # verso_2 is distributed in the hope that it will be useful, but WITHOUT ANY 22 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 23 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 24 # details. 25 # 26 # You should have received a copy of the GNU General Public License 27 # along with verso_2. If not, see <https://www.gnu.org/licenses/>. 28 # 29 # ============================================================================= 30 31 set -e 32 33 # Load configuration 34 if [ ! -f "./verso.conf" ]; then 35 echo "Error: verso.conf not found in current directory" 36 exit 1 37 fi 38 . ./verso.conf 39 40 # Helper functions 41 log() { 42 printf "* %s\n" "$1" 43 } 44 45 error() { 46 printf "Error: %s\n" "$1" >&2 47 exit 1 48 } 49 50 # Extract metadata from markdown file 51 # Supports simple YAML frontmatter between --- markers 52 extract_meta() { 53 local file="$1" 54 local key="$2" 55 56 awk -v key="$key" ' 57 /^---$/ { in_meta = !in_meta; next } 58 in_meta && $0 ~ "^" key ":" { 59 sub("^" key ": *", "") 60 print 61 exit 62 } 63 ' "$file" 64 } 65 66 # Extract first h1 title from markdown 67 extract_title() { 68 local file="$1" 69 local meta_title 70 71 # Try frontmatter first 72 meta_title=$(extract_meta "$file" "title") 73 if [ -n "$meta_title" ]; then 74 echo "$meta_title" 75 return 76 fi 77 78 # Fall back to first # heading 79 grep '^# ' "$file" | head -n 1 | sed 's/^# *//' || echo "Untitled" 80 } 81 82 # Extract date from frontmatter or filename 83 extract_date() { 84 local file="$1" 85 local meta_date 86 local basename_file 87 88 # Try frontmatter first 89 meta_date=$(extract_meta "$file" "date") 90 if [ -n "$meta_date" ]; then 91 echo "$meta_date" 92 return 93 fi 94 95 # Try to extract from filename (YYYY-MM-DD-title.md format) 96 basename_file=$(basename "$file") 97 echo "$basename_file" | grep -oE '^[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo "" 98 } 99 100 extract_author() { 101 extract_meta "$1" "author" 102 } 103 104 # Substitute template variables 105 substitute_vars() { 106 local template="$1" 107 local title="$2" 108 local content="$3" 109 local date="$4" 110 local nav="$5" 111 local author="$6" 112 local show_meta="$7" 113 114 # First pass: substitute simple variables 115 sed -e "s|{{TITLE}}|${title}|g" \ 116 -e "s|{{SITE_TITLE}}|${SITE_TITLE}|g" \ 117 -e "s|{{SITE_URL}}|${SITE_URL}|g" \ 118 -e "s|{{SITE_DESCRIPTION}}|${SITE_DESCRIPTION}|g" \ 119 -e "s|{{AUTHOR}}|${author}|g" \ 120 -e "s|{{DATE}}|${date}|g" \ 121 "$template" | while IFS= read -r line; do 122 123 # 1. Handle metadata toggle 124 if echo "$line" | grep -q '{{META_LINE}}'; then 125 if [ "$show_meta" != "no" ]; then 126 echo "<p class=\"meta\">$author --- $date</p>" 127 fi 128 # 2. Handle {{NAV}} substitution 129 elif echo "$line" | grep -q '{{NAV}}'; then 130 echo "$nav" 131 # 3. Handle {{CONTENT}} substitution 132 elif echo "$line" | grep -q '{{CONTENT}}'; then 133 cat "$content" 134 # 4. Otherwise, print the line as is 135 else 136 echo "$line" 137 fi 138 done 139 } 140 141 # Calculate relative path back to root 142 get_relpath() { 143 local path="$1" 144 local depth 145 146 depth=$(echo "$path" | grep -o "/" | wc -l) 147 if [ "$depth" -eq 0 ]; then 148 echo "." 149 else 150 printf '../%.0s' $(seq 1 "$depth") | sed 's/\/$//' 151 fi 152 } 153 154 # Build a single page 155 build_page() { 156 local src="$1" 157 local dst="$2" 158 local rel_src 159 local title 160 local date 161 local nav 162 local tmpfile 163 local author 164 local show_meta 165 166 rel_src=$(echo "$src" | sed "s|^${INPUT_DIR}/||") 167 title=$(extract_title "$src") 168 date=$(extract_date "$src") 169 nav=$(generate_nav) 170 author=$(extract_author "$src") 171 show_meta=$(extract_meta "$src" "show_meta") 172 173 [ -z "$author" ] && author="$AUTHOR" 174 175 # Create output directory 176 mkdir -p "$(dirname "$dst")" 177 178 # Strip frontmatter and render markdown to temp file 179 tmpfile=$(mktemp) 180 awk ' 181 /^---$/ { 182 if (!seen_first) { 183 seen_first = 1; 184 in_meta = 1; 185 next 186 } else if (in_meta) { 187 in_meta = 0; 188 next 189 } 190 } 191 !in_meta { print } 192 ' "$src" | $MD_PROCESSOR $MD_FLAGS > "$tmpfile" 193 194 # Substitute and write output 195 substitute_vars "$TEMPLATE_DIR/header.html" "$title" "$tmpfile" "$date" "$nav" "$author" "$show_meta"> "$dst" 196 197 # Add footer with substitution 198 sed -e "s|{{AUTHOR}}|${AUTHOR}|g" \ 199 -e "s|{{SITE_TITLE}}|${SITE_TITLE}|g" \ 200 "$TEMPLATE_DIR/footer.html" >> "$dst" 201 202 rm "$tmpfile" 203 log "Built: $rel_src -> $(echo "$dst" | sed "s|^${OUTPUT_DIR}/||")" 204 } 205 206 # Generate navigation menu 207 generate_nav() { 208 local nav_html="" 209 210 # Find all items in root of INPUT_DIR (both files and directories) 211 ( 212 # List markdown files (excluding index.md) 213 find "$INPUT_DIR" -maxdepth 1 -name "*.md" ! -name "index.md" -type f | while read f; do 214 title=$(extract_title "$f") 215 slug=$(basename "$f" .md) 216 echo "file|$slug|$title" 217 done 218 219 # List directories 220 find "$INPUT_DIR" -maxdepth 1 -type d ! -path "$INPUT_DIR" | while read d; do 221 dirname=$(basename "$d") 222 # Skip assets directory 223 [ "$dirname" = "assets" ] && continue 224 225 # Try to get title from index.md if it exists 226 if [ -f "$d/index.md" ]; then 227 title=$(extract_title "$d/index.md") 228 else 229 # Convert dirname to title (capitalize, replace hyphens/underscores) 230 title=$(echo "$dirname" | sed 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1') 231 fi 232 echo "dir|$dirname|$title" 233 done 234 ) | sort -t'|' -k2 | while IFS='|' read type slug title; do 235 if [ "$CLEAN_URLS" = "yes" ]; then 236 nav_html="$nav_html <a href=\"/$slug/\">$title</a>" 237 else 238 if [ "$type" = "dir" ]; then 239 nav_html="$nav_html <a href=\"/$slug.html\">$title</a>" 240 else 241 nav_html="$nav_html <a href=\"/$slug.html\">$title</a>" 242 fi 243 fi 244 245 echo "$nav_html" 246 nav_html="" 247 done 248 249 echo " <a href=\"/feed.xml\">RSS Feed</a>" 250 echo " <a href=\"${AUTHOR_GIT_HOST}\">Git</a>" 251 } 252 253 generate_directory_index() { 254 local dir="$1" 255 local output="$2" 256 local dirname=$(basename "$dir") 257 local title 258 local index_md 259 260 # Convert dirname to title 261 title=$(echo "$dirname" | sed 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1') 262 263 log "Auto-generating index for $dirname..." 264 265 index_md=$(mktemp) 266 267 # ADD THIS BLOCK HERE: 268 cat << EOF > "$index_md" 269 --- 270 title: $title 271 show_meta: no 272 --- 273 EOF 274 # Note: Use >> for the rest so we don't overwrite the frontmatter! 275 276 echo "" >> "$index_md" 277 278 # Find all markdown files, extract date, sort, then format into markdown 279 find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -type f | while read f; do 280 file_title=$(extract_title "$f") 281 date=$(extract_date "$f") 282 slug=$(basename "$f" .md) 283 # Output: date|title|slug for sorting 284 echo "${date}|${file_title}|${slug}" 285 done | sort -r | while IFS='|' read date title slug; do 286 if [ -n "$date" ]; then 287 echo "* $date [$title]($slug/)" >> "$index_md" 288 else 289 echo "* [$title]($slug/)" >> "$index_md" 290 fi 291 done 292 293 # ... (rest of the subdirectory finding logic) 294 295 # Build the page 296 build_page "$index_md" "$output" 297 rm "$index_md" 298 } 299 300 extract_article_html() { 301 sed -n '/<article>/,/<\/article>/ { 302 /<header class="post-meta">/,/<\/header>/d 303 p 304 }' "$1" 305 } 306 307 # Generate blog index 308 generate_blog_index() { 309 local blog_src="$INPUT_DIR/$BLOG_DIR" 310 local blog_dst="$OUTPUT_DIR/$BLOG_DIR" 311 local index_md 312 313 [ ! -d "$blog_src" ] && return 314 315 log "Generating blog index..." 316 317 index_md=$(mktemp) 318 319 # Use > for the first line to create/clear, then >> for everything else 320 echo "---" > "$index_md" 321 echo "show_meta: no" >> "$index_md" 322 echo "title: Blog" >> "$index_md" 323 echo "---" >> "$index_md" 324 echo "" >> "$index_md" 325 echo "# Blog" >> "$index_md" 326 echo "" >> "$index_md" 327 echo "[RSS Feed](../feed.xml)" >> "$index_md" 328 echo "" >> "$index_md" 329 330 # Find all blog posts, sort by date descending 331 find "$blog_src" -name "*.md" ! -name "index.md" | while read f; do 332 date=$(extract_date "$f") 333 title=$(extract_title "$f") 334 slug=$(basename "$f" .md) 335 echo "${date}|${title}|${slug}" 336 done | sort -r | while IFS='|' read date title slug; do 337 if [ "$CLEAN_URLS" = "yes" ]; then 338 echo "* $date [$title]($slug/)" >> "$index_md" 339 else 340 echo "* $date [$title]($slug.html)" >> "$index_md" 341 fi 342 done 343 344 # Build the index page 345 if [ "$CLEAN_URLS" = "yes" ]; then 346 build_page "$index_md" "$blog_dst/index.html" 347 else 348 build_page "$index_md" "$blog_dst.html" 349 fi 350 351 rm "$index_md" 352 } 353 354 # Generate RSS feed 355 generate_rss() { 356 local rss_file="$OUTPUT_DIR/feed.xml" 357 local blog_src="$INPUT_DIR/$BLOG_DIR" 358 359 [ ! -d "$blog_src" ] && return 360 [ "$GENERATE_RSS" != "yes" ] && return 361 362 log "Generating RSS feed..." 363 364 mkdir -p "$(dirname "$rss_file")" 365 366 cat > "$rss_file" << EOF 367 <?xml version="1.0" encoding="UTF-8"?> 368 <rss version="2.0"> 369 <channel> 370 <title>${SITE_TITLE}</title> 371 <link>${SITE_URL}</link> 372 <description>${SITE_DESCRIPTION}</description> 373 <language>en</language> 374 EOF 375 376 # Add items 377 find "$blog_src" -name "*.md" ! -name "index.md" | while read f; do 378 date=$(extract_date "$f") 379 title=$(extract_title "$f") 380 slug=$(basename "$f" .md) 381 author=$(extract_author "$f") 382 [ -z "$author" ] && author="$AUTHOR" 383 384 385 echo "${date}|${title}|${slug}|${f}" 386 done | sort -r | head -n 20 | while IFS='|' read date title slug file; do 387 if [ "$CLEAN_URLS" = "yes" ]; then 388 url="${SITE_URL}/${BLOG_DIR}/${slug}/" 389 else 390 url="${SITE_URL}/${BLOG_DIR}/${slug}.html" 391 fi 392 393 html_file="$OUTPUT_DIR/$BLOG_DIR/$slug/index.html" 394 body_html=$(extract_article_html "$html_file") 395 396 cat >> "$rss_file" << EOF 397 <item> 398 <title>${title}</title> 399 <link>${url}</link> 400 <guid>${url}</guid> 401 <pubDate>${date}</pubDate> 402 <author>${AUTHOR}</author> 403 <description><![CDATA[ 404 ${body_html} 405 ]]></description> 406 </item> 407 EOF 408 done 409 410 echo "</channel>" >> "$rss_file" 411 echo "</rss>" >> "$rss_file" 412 } 413 414 # Copy static assets 415 copy_assets() { 416 log "Copying assets..." 417 418 for ext in $COPY_EXTENSIONS; do 419 find "$INPUT_DIR" -type f -name "*.$ext" | while read f; do 420 rel_path=$(echo "$f" | sed "s|^${INPUT_DIR}/||") 421 dst="$OUTPUT_DIR/$rel_path" 422 423 # Skip if matches ignore pattern 424 skip=0 425 for pattern in $IGNORE_PATTERNS; do 426 if echo "$rel_path" | grep -qE "$pattern"; then 427 skip=1 428 break 429 fi 430 done 431 432 [ $skip -eq 1 ] && continue 433 434 mkdir -p "$(dirname "$dst")" 435 cp "$f" "$dst" 436 log "Copied: $rel_path" 437 done 438 done 439 } 440 441 # Initialize output directory 442 init_output() { 443 log "Initializing output directory..." 444 rm -rf "$OUTPUT_DIR" 445 mkdir -p "$OUTPUT_DIR" 446 } 447 448 # Build all pages 449 build_all() { 450 log "Building pages..." 451 452 # First, build all existing markdown files 453 find "$INPUT_DIR" -type f -name "*.md" | while read f; do 454 rel_path=$(echo "$f" | sed "s|^${INPUT_DIR}/||" | sed 's/\.md$//') 455 456 # Skip if matches ignore pattern 457 skip=0 458 for pattern in $IGNORE_PATTERNS; do 459 if echo "$rel_path" | grep -qE "$pattern"; then 460 skip=1 461 break 462 fi 463 done 464 465 [ $skip -eq 1 ] && continue 466 467 # Handle index.md files specially (both root and subdirectory) 468 if echo "$rel_path" | grep -q '/index$'; then 469 # Subdirectory index: blog/index.md -> blog/index.html 470 dir_path=$(dirname "$rel_path") 471 if [ "$CLEAN_URLS" = "yes" ]; then 472 dst="$OUTPUT_DIR/$dir_path/index.html" 473 else 474 dst="$OUTPUT_DIR/$dir_path.html" 475 fi 476 elif [ "$rel_path" = "index" ]; then 477 # Root index: index.md -> index.html 478 dst="$OUTPUT_DIR/index.html" 479 elif [ "$CLEAN_URLS" = "yes" ]; then 480 dst="$OUTPUT_DIR/$rel_path/index.html" 481 else 482 dst="$OUTPUT_DIR/$rel_path.html" 483 fi 484 485 build_page "$f" "$dst" 486 done 487 488 # Now check for directories that need auto-generated indexes 489 find "$INPUT_DIR" -type d ! -path "$INPUT_DIR" | while read dir; do 490 rel_dir=$(echo "$dir" | sed "s|^${INPUT_DIR}/||") 491 492 # Skip assets directory 493 [ "$rel_dir" = "assets" ] && continue 494 495 # Skip if matches ignore pattern 496 skip=0 497 for pattern in $IGNORE_PATTERNS; do 498 if echo "$rel_dir" | grep -qE "$pattern"; then 499 skip=1 500 break 501 fi 502 done 503 504 [ $skip -eq 1 ] && continue 505 506 # Check if index.md exists 507 if [ ! -f "$dir/index.md" ]; then 508 # Generate auto-index 509 if [ "$CLEAN_URLS" = "yes" ]; then 510 dst="$OUTPUT_DIR/$rel_dir/index.html" 511 else 512 dst="$OUTPUT_DIR/$rel_dir.html" 513 fi 514 515 generate_directory_index "$dir" "$dst" 516 fi 517 done 518 } 519 520 # Main execution 521 main() { 522 # Check for markdown processor 523 if ! command -v "$MD_PROCESSOR" > /dev/null 2>&1; then 524 error "Markdown processor '$MD_PROCESSOR' not found. Install it or change MD_PROCESSOR in verso.conf" 525 fi 526 527 # Check templates exist 528 if [ ! -f "$TEMPLATE_DIR/header.html" ] || [ ! -f "$TEMPLATE_DIR/footer.html" ]; then 529 error "Templates not found in $TEMPLATE_DIR/" 530 fi 531 532 init_output 533 build_all 534 copy_assets 535 generate_rss 536 537 log "Done! Site built in $OUTPUT_DIR/" 538 } 539 540 main