// Utility functions for the static site generator // Escape HTML special characters pub fn escapeHtml(s: String): String = String.replace(String.replace(String.replace(s, "&", "&"), "<", "<"), ">", ">") // Extract slug from a filename like "2025-01-29-blog-revamped.md" // Returns "2025-01-29-blog-revamped" pub fn slugFromFilename(filename: String): String = { if String.endsWith(filename, ".md") then String.substring(filename, 0, String.length(filename) - 3) else filename } // Format ISO date "2025-01-29" to "2025 Jan 29" pub fn formatDate(isoDate: String): String = { if String.length(isoDate) < 10 then isoDate else { let year = String.substring(isoDate, 0, 4); let month = String.substring(isoDate, 5, 7); let day = String.substring(isoDate, 8, 10); let monthName = if month == "01" then "Jan" else if month == "02" then "Feb" else if month == "03" then "Mar" else if month == "04" then "Apr" else if month == "05" then "May" else if month == "06" then "Jun" else if month == "07" then "Jul" else if month == "08" then "Aug" else if month == "09" then "Sep" else if month == "10" then "Oct" else if month == "11" then "Nov" else "Dec"; year + " " + monthName + " " + day } } // Recursively walk a directory, returning all file paths pub fn walkDir(path: String): List with {File} = { let entries = File.readDir(path); List.fold(entries, [], fn(acc: List, entry: String): List => { let fullPath = path + "/" + entry; if File.isDir(fullPath) then List.concat(acc, walkDir(fullPath)) else List.concat(acc, [fullPath]) }) } // Insertion sort by date string (descending — newest first) // Each item is (date, item) tuple pub fn sortByDateDesc(items: List<(String, String, String, String, String)>): List<(String, String, String, String, String)> = { List.fold(items, [], fn(sorted: List<(String, String, String, String, String)>, item: (String, String, String, String, String)): List<(String, String, String, String, String)> => insertByDate(sorted, item) ) } fn insertByDate(sorted: List<(String, String, String, String, String)>, item: (String, String, String, String, String)): List<(String, String, String, String, String)> = { let itemDate = item.0; match List.head(sorted) { None => [item], Some(first) => { let firstDate = first.0; // Compare dates as strings — ISO format sorts lexicographically if itemDate >= firstDate then List.concat([item], sorted) else match List.tail(sorted) { Some(rest) => List.concat([first], insertByDate(rest, item)), None => [first, item] } } } } // Get just the filename from a path pub fn basename(path: String): String = { match String.lastIndexOf(path, "/") { Some(idx) => String.substring(path, idx + 1, String.length(path)), None => path } } // Get the directory part of a path pub fn dirname(path: String): String = { match String.lastIndexOf(path, "/") { Some(idx) => String.substring(path, 0, idx), None => "." } }