Add ssg package to registry
Static site generator utilities extracted from blu-site, providing reusable post/tag types, content reading, date sorting, and file I/O. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ lux pkg add locallib --path ../mylib
|
|||||||
| [xml](./packages/xml/) | 0.1.0 | XML builder |
|
| [xml](./packages/xml/) | 0.1.0 | XML builder |
|
||||||
| [rss](./packages/rss/) | 0.1.0 | RSS 2.0 feed generator |
|
| [rss](./packages/rss/) | 0.1.0 | RSS 2.0 feed generator |
|
||||||
| [web](./packages/web/) | 0.1.0 | Full-stack web framework |
|
| [web](./packages/web/) | 0.1.0 | Full-stack web framework |
|
||||||
|
| [ssg](./packages/ssg/) | 0.1.0 | Static site generator utilities |
|
||||||
|
|
||||||
## Publishing Packages
|
## Publishing Packages
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,14 @@
|
|||||||
"versions": [
|
"versions": [
|
||||||
{"version": "0.1.0", "checksum": "", "published_at": "2026-02-24", "yanked": false}
|
{"version": "0.1.0", "checksum": "", "published_at": "2026-02-24", "yanked": false}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ssg",
|
||||||
|
"description": "Static site generator utilities for Lux",
|
||||||
|
"latest_version": "0.1.0",
|
||||||
|
"versions": [
|
||||||
|
{"version": "0.1.0", "checksum": "", "published_at": "2026-02-24", "yanked": false}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
207
packages/ssg/lib.lux
Normal file
207
packages/ssg/lib.lux
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// ssg - Static site generator utilities for Lux
|
||||||
|
//
|
||||||
|
// Provides reusable types and functions for building static sites:
|
||||||
|
// - Post/TagEntry types for content modeling
|
||||||
|
// - File reading and frontmatter parsing
|
||||||
|
// - Date sorting and formatting
|
||||||
|
// - Tag collection and filtering
|
||||||
|
// - File I/O helpers
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
import frontmatter
|
||||||
|
import path
|
||||||
|
|
||||||
|
// A parsed post: date, title, slug, tags (raw string), content (HTML)
|
||||||
|
pub type Post = | Post(String, String, String, String, String)
|
||||||
|
|
||||||
|
// A tag entry: tag name, post title, post date, post slug, section name
|
||||||
|
pub type TagEntry = | TagEntry(String, String, String, String, String)
|
||||||
|
|
||||||
|
// --- Post Accessors ---
|
||||||
|
|
||||||
|
pub fn postDate(p: Post): String =
|
||||||
|
match p {
|
||||||
|
Post(d, _, _, _, _) => d,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postTitle(p: Post): String =
|
||||||
|
match p {
|
||||||
|
Post(_, t, _, _, _) => t,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postSlug(p: Post): String =
|
||||||
|
match p {
|
||||||
|
Post(_, _, s, _, _) => s,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postTags(p: Post): String =
|
||||||
|
match p {
|
||||||
|
Post(_, _, _, t, _) => t,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postContent(p: Post): String =
|
||||||
|
match p {
|
||||||
|
Post(_, _, _, _, c) => c,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postTagList(p: Post): List<String> = {
|
||||||
|
let raw = postTags(p)
|
||||||
|
if raw == "" then [] else String.split(raw, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TagEntry Accessors ---
|
||||||
|
|
||||||
|
pub fn tagName(e: TagEntry): String =
|
||||||
|
match e {
|
||||||
|
TagEntry(t, _, _, _, _) => t,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tagTitle(e: TagEntry): String =
|
||||||
|
match e {
|
||||||
|
TagEntry(_, t, _, _, _) => t,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tagDate(e: TagEntry): String =
|
||||||
|
match e {
|
||||||
|
TagEntry(_, _, d, _, _) => d,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tagSlug(e: TagEntry): String =
|
||||||
|
match e {
|
||||||
|
TagEntry(_, _, _, s, _) => s,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tagSection(e: TagEntry): String =
|
||||||
|
match e {
|
||||||
|
TagEntry(_, _, _, _, s) => s,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Slug / Filename ---
|
||||||
|
|
||||||
|
pub fn slugFromFilename(filename: String): String = path.stripExtension(filename)
|
||||||
|
|
||||||
|
// --- Date Formatting ---
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sorting ---
|
||||||
|
|
||||||
|
fn insertByDate(sorted: List<Post>, item: Post): List<Post> = {
|
||||||
|
match List.head(sorted) {
|
||||||
|
None => [item],
|
||||||
|
Some(first) => if postDate(item) >= postDate(first) then List.concat([item], sorted) else match List.tail(sorted) {
|
||||||
|
Some(rest) => List.concat([first], insertByDate(rest, item)),
|
||||||
|
None => [first, item],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sortInsert(sorted: List<Post>, item: Post): List<Post> = insertByDate(sorted, item)
|
||||||
|
|
||||||
|
pub fn sortByDateDesc(items: List<Post>): List<Post> = List.fold(items, [], sortInsert)
|
||||||
|
|
||||||
|
// --- Content Reading ---
|
||||||
|
|
||||||
|
pub fn parseFile(filepath: String): Post with {File} = {
|
||||||
|
let raw = File.read(filepath)
|
||||||
|
let doc = frontmatter.parse(raw)
|
||||||
|
let title = frontmatter.title(doc)
|
||||||
|
let date = frontmatter.date(doc)
|
||||||
|
let tags = frontmatter.getOrDefault(doc, "tags", "")
|
||||||
|
let body = frontmatter.body(doc)
|
||||||
|
let htmlContent = markdown.toHtml(body)
|
||||||
|
let filename = path.basename(filepath)
|
||||||
|
let slug = slugFromFilename(filename)
|
||||||
|
Post(date, title, slug, tags, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mapParseFiles(dir: String, files: List<String>): List<Post> with {File} =
|
||||||
|
match List.head(files) {
|
||||||
|
None => [],
|
||||||
|
Some(filename) => {
|
||||||
|
let page = parseFile(dir + "/" + filename)
|
||||||
|
match List.tail(files) {
|
||||||
|
Some(rest) => List.concat([page], mapParseFiles(dir, rest)),
|
||||||
|
None => [page],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readSection(contentDir: String, section: String): List<Post> with {File} = {
|
||||||
|
let dir = contentDir + "/" + section
|
||||||
|
if File.exists(dir) then {
|
||||||
|
let entries = File.readDir(dir)
|
||||||
|
let mdFiles = List.filter(entries, fn(e: String): Bool => String.endsWith(e, ".md"))
|
||||||
|
mapParseFiles(dir, mdFiles)
|
||||||
|
} else []
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAllSections(contentDir: String, sections: List<String>): List<Post> with {File} =
|
||||||
|
match List.head(sections) {
|
||||||
|
None => [],
|
||||||
|
Some(section) => {
|
||||||
|
let posts = readSection(contentDir, section)
|
||||||
|
match List.tail(sections) {
|
||||||
|
Some(rest) => List.concat(posts, readAllSections(contentDir, rest)),
|
||||||
|
None => posts,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tags ---
|
||||||
|
|
||||||
|
fn collectTagsForPage(section: String, page: Post): List<TagEntry> = {
|
||||||
|
let tagsRaw = postTags(page)
|
||||||
|
let tags = if tagsRaw == "" then [] else String.split(tagsRaw, " ")
|
||||||
|
List.map(tags, fn(tag: String): TagEntry => TagEntry(tag, postTitle(page), postDate(page), postSlug(page), section))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collectTags(section: String, posts: List<Post>): List<TagEntry> = {
|
||||||
|
let nested = List.map(posts, fn(page: Post): List<TagEntry> => collectTagsForPage(section, page))
|
||||||
|
List.fold(nested, [], fn(acc: List<TagEntry>, entries: List<TagEntry>): List<TagEntry> => List.concat(acc, entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collectAllTags(sections: List<String>, contentDir: String): List<TagEntry> with {File} =
|
||||||
|
match List.head(sections) {
|
||||||
|
None => [],
|
||||||
|
Some(section) => {
|
||||||
|
let posts = readSection(contentDir, section)
|
||||||
|
let tags = collectTags(section, posts)
|
||||||
|
match List.tail(sections) {
|
||||||
|
Some(rest) => List.concat(tags, collectAllTags(rest, contentDir)),
|
||||||
|
None => tags,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addIfUnique(acc: List<String>, e: TagEntry): List<String> =
|
||||||
|
if List.any(acc, fn(t: String): Bool => t == tagName(e)) then acc else List.concat(acc, [tagName(e)])
|
||||||
|
|
||||||
|
pub fn uniqueTags(entries: List<TagEntry>): List<String> = List.fold(entries, [], addIfUnique)
|
||||||
|
|
||||||
|
pub fn entriesForTag(entries: List<TagEntry>, tag: String): List<TagEntry> =
|
||||||
|
List.filter(entries, fn(e: TagEntry): Bool => tagName(e) == tag)
|
||||||
|
|
||||||
|
// --- File I/O ---
|
||||||
|
|
||||||
|
pub fn ensureDir(dirPath: String): Unit with {File} = {
|
||||||
|
if File.exists(dirPath) then () else {
|
||||||
|
let parent = path.dirname(dirPath)
|
||||||
|
if parent != "." then if parent != dirPath then ensureDir(parent) else () else ()
|
||||||
|
File.mkdir(dirPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writePage(outputPath: String, content: String): Unit with {File} = {
|
||||||
|
let dir = path.dirname(outputPath)
|
||||||
|
if dir != "." then ensureDir(dir) else ()
|
||||||
|
File.write(outputPath, content)
|
||||||
|
}
|
||||||
11
packages/ssg/lux.toml
Normal file
11
packages/ssg/lux.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[project]
|
||||||
|
name = "ssg"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Static site generator utilities for Lux"
|
||||||
|
authors = ["Brandon Lucas"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
frontmatter = { version = "0.1.0", path = "../frontmatter" }
|
||||||
|
markdown = { version = "0.1.0", path = "../markdown" }
|
||||||
|
path = { version = "0.1.0", path = "../path" }
|
||||||
Reference in New Issue
Block a user