feat: add blu-site static site generator and fix language issues
Build a complete static site generator in Lux that faithfully clones
blu.cx (elmstatic). Generates 14 post pages, section indexes, tag pages,
and a home page with snippets grid from markdown content.
Language fixes discovered during development:
- Add \{ and \} escape sequences in string literals (lexer)
- Register String.indexOf and String.lastIndexOf in type checker
- Fix formatter to preserve brace escapes in string literals
- Improve LSP hover to show documentation for let bindings and functions
ISSUES.md documents 15 Lux language limitations found during the project.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
91
projects/blu-site/util.lux
Normal file
91
projects/blu-site/util.lux
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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<String> with {File} = {
|
||||
let entries = File.readDir(path);
|
||||
List.fold(entries, [], fn(acc: List<String>, entry: String): List<String> => {
|
||||
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 => "."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user