verso2

A simple web framework.
git clone git://git.chappelle.dev/verso2.git
Log | Files | Refs | README | LICENSE

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