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:
2026-02-17 15:43:05 -05:00
parent db82ca1a1c
commit bac63bab2a
99 changed files with 5335 additions and 99 deletions

View File

@@ -0,0 +1,79 @@
// YAML-like frontmatter parser
// Parses the --- delimited frontmatter block from markdown files
pub type Frontmatter =
| Frontmatter(String, String, String, String)
// title date desc tagsRaw
pub type ParseResult =
| ParseResult(Frontmatter, String)
// front body
pub fn parse(content: String): ParseResult = {
let lines = String.lines(content);
// Skip first --- line, collect key: value pairs until next ---
let result = List.fold(lines, (false, false, "", "", "", "", ""), fn(acc: (Bool, Bool, String, String, String, String, String), line: String): (Bool, Bool, String, String, String, String, String) => {
let inFront = acc.0;
let pastFront = acc.1;
let title = acc.2;
let date = acc.3;
let desc = acc.4;
let tags = acc.5;
let body = acc.6;
if pastFront then
(inFront, pastFront, title, date, desc, tags, body + line + "\n")
else if String.trim(line) == "---" then
if inFront then
// End of frontmatter
(false, true, title, date, desc, tags, body)
else
// Start of frontmatter
(true, false, title, date, desc, tags, body)
else if inFront then {
// Parse key: value
match String.indexOf(line, ": ") {
Some(idx) => {
let key = String.trim(String.substring(line, 0, idx));
let rawVal = String.trim(String.substring(line, idx + 2, String.length(line)));
// Strip surrounding quotes if present
let val = if String.startsWith(rawVal, "\"") then
String.substring(rawVal, 1, String.length(rawVal) - 1)
else
rawVal;
if key == "title" then
(inFront, pastFront, val, date, desc, tags, body)
else if key == "date" then
(inFront, pastFront, title, val, desc, tags, body)
else if key == "description" then
(inFront, pastFront, title, date, val, tags, body)
else if key == "tags" then
(inFront, pastFront, title, date, desc, val, body)
else
(inFront, pastFront, title, date, desc, tags, body)
},
None => (inFront, pastFront, title, date, desc, tags, body)
}
} else
(inFront, pastFront, title, date, desc, tags, body)
});
let front = Frontmatter(result.2, result.3, result.4, result.5);
ParseResult(front, result.6)
}
pub fn getTitle(f: Frontmatter): String =
match f { Frontmatter(t, _, _, _) => t }
pub fn getDate(f: Frontmatter): String =
match f { Frontmatter(_, d, _, _) => d }
pub fn getDesc(f: Frontmatter): String =
match f { Frontmatter(_, _, d, _) => d }
pub fn getTagsRaw(f: Frontmatter): String =
match f { Frontmatter(_, _, _, t) => t }
pub fn getTags(f: Frontmatter): List<String> =
match f { Frontmatter(_, _, _, t) =>
if t == "" then []
else String.split(t, " ")
}