Skip to content

Instantly share code, notes, and snippets.

@nicebyte
Last active December 13, 2024 22:31
Show Gist options
  • Save nicebyte/86bd4e9f731e5536e1f239bd53d59837 to your computer and use it in GitHub Desktop.
Save nicebyte/86bd4e9f731e5536e1f239bd53d59837 to your computer and use it in GitHub Desktop.
lua site generator
-- Usage:
-- lua generator.lua --src <path-to-source-folder>
-- --dst <path-to-destination-folder>
-- --assets <paths-to-asset-subfolders>
-- --content <paths-to-content-subfolders>
--
-- The source folder must contain all templates within a subfolder called
-- "templates".
-- The destination folder must exist prior to invokation of the script.
-- "--assets" specifies names of subfolders in the source folder that will get
-- copied to the destination.
-- "--content" specifies names of subfolders in the source folder that contain
-- content files from which HTML will be generated using templates. Note that
-- "." is a valid content subfolder, allowing to place output files in the root.
-- Content files are lua files comprising a single Lua table in the following
-- format:
-- {
-- ["title"] = "<content title>",
-- ["pubdate"] = {["month"]=<publication day month 1-12>, ["day"]=<publication day of the month>, ["year"]=<publication year>},
-- ["template"] = "<name of the template to use from the templates subfolder>,
-- ["slug"] = "<name of the output file that will be used verbatim. if not specified, it will be generated>"
-- ["content"] = [[
-- <raw html goes here>
-- ]]
-- }
-- Templates are lua scripts that specify how the content is presented. Example
-- template:
-- t:render("header.html") -- templates can include other templates
-- t:sink([[
-- <h1>%s</h1>
-- <p><small>published on %s</small></p>
-- %s
-- <hr style="border: 1px dotted #353535; border-bottom: none;"/>
-- <p><i>Like this post? <a href="https://bsky.app/profile/nicebyte.bsky.social">Follow</a> me on bluesky for more!</i></p>
-- ]], t.data.title, os.date("%b %d %Y", t.data.timestamp), t.data.content) -- t:sink writes a formatted string to output file
-- t:render("footer.html")
-- In the context of a template, `t.data` is the table read from the content file.
-- This script requires Lua FileSystem. If you're not using a custom Lua build
-- with Lua FileSystem hacked in, uncomment the following line:
-- local lfs = require("lfs")
function bg_fatal(msg)
print("FATAL ERROR: " .. msg)
os.exit(1)
end
function bg_parse_cmdline(args)
local src_key <const> = "src"
local dst_key <const> = "dst"
local content_key <const> = "content"
local asset_key <const> = "assets"
local result = {
["content_dirs"] = {},
["asset_dirs"] = {}
}
local append_list = nil
for arg_idx=0,#args do
local arg = args[arg_idx]
if arg:sub(1,2) == "--" then
append_list = nil
local arg_key = arg:sub(3)
if arg_key == src_key or arg_key == dst_key then
if arg_idx == #args then bg_fatal("missing argument value") end
local arg_value = args[arg_idx + 1]
result[arg_key == src_key and "src" or "dst"] = arg_value
elseif arg_key == content_key then
append_list = result.content_dirs
elseif arg_key == asset_key then
append_list = result.asset_dirs
end
elseif append_list then
append_list[#append_list+1] = arg
end
end
if not result.src or not result.dst then
bg_fatal("source or destination folder not specified")
end
return result
end
function bg_isdir(path)
mode, _ = lfs.attributes(path, "mode")
return mode == "directory"
end
function bg_pathsep()
return "/"
end
string.find_last_sub = function(str, pattern, init)
local s, e = nil, nil
local found_last = false
while not found_last do
local sn, en = string.find(str, pattern, init, true)
found_last = not sn
if sn then
s, e = sn, en
init = s + 1
end
end
return s,e
end
function bg_makepath(...)
local components = {...}
local lastc = components[#components]
local trailingsep = lastc:find_last_sub(bg_pathsep())
components[#components] = lastc:sub(1, trailingsep and trailingsep-1 or nil)
return table.concat(components, bg_pathsep())
end
function bg_validate_settings(settings)
if not bg_isdir(settings.src) then
bg_fatal(settings.src.." is not a folder")
end
if not bg_isdir(settings.dst) then
bg_fatal(settings.dst.." is not a folder")
end
print("source folder path: "..settings.src)
print("destination folder path: "..settings.dst)
return settings
end
function bg_iterate_files(path, func)
if not bg_isdir(path) then
bg_fatal("directory "..path.." not found")
end
local dir_it, dir = lfs.dir(path)
local it = function() return dir_it(dir) end
for basename in it do
local file_path = bg_makepath(path, basename)
if lfs.attributes(file_path, "mode") == "file" then
func(basename, file_path)
end
end
end
function bg_readfile(path)
local f = io.open(path, "r")
if not f then bg_fatal("failed to open "..template_path) end
local content = f:read("*all")
f:close()
return content
end
function bg_load_code(prefix, source_file)
return load((function()
local load_stage = 0
return function()
load_stage = load_stage + 1
return ({
[1] = function() return prefix end,
[2] = function() return bg_readfile(source_file) end,
[3] = function() return nil end
})[load_stage]()
end
end)())
end
function bg_load_templates(source)
result = {}
bg_iterate_files(
bg_makepath(source, "templates"),
function(template_basename, template_path)
local t, err = bg_load_code("local t=({...})[1]", template_path)
if not t then bg_fatal("failed to load template \""..template_path.."\", error"..err) end
result[template_basename] = t
end)
return result
end
function bg_load_content(srcdir, sources)
result = {}
for _,source in pairs(sources) do
bg_iterate_files(
bg_makepath(srcdir, source),
function (content_basename, content_path)
local chunk, err = bg_load_code("return ", content_path)
if not chunk then bg_fatal("failed to load \""..content_path.."\", error: "..err) end
local data = chunk()
if not data then bg_fatal(content_path.." did not return any data") end
if not data.title then bg_fatal(content_path.." is missing title") end
if not data.content then bg_fatal(content_path.." is missing content") end
if not data.pubdate then bg_fatal(content_path.." is missing publication date") end
if not data.template then bg_fatal(content_path.." is missing a template") end
if not data.pubdate.month or
not data.pubdate.day or
not data.pubdate.year then
bg_fatal(content_path.." bad publication date")
end
data.timestamp = os.time(data.pubdate)
if not data.slug then
local last_dot_pos = content_basename:find_last_sub(".")
data.slug =
((last_dot_pos and last_dot_pos > 1)
and string.sub(content_basename, 1, last_dot_pos - 1)
or content_basename)..".html"
end
data.rel_out_path = source
if not data.tags then data.tags = {} end
result[#result+1] = data
end)
end
table.sort(result, function(a,b) return a.timestamp > b.timestamp end)
return result
end
function bg_copydir(source, dest)
local dir_it, dir = lfs.dir(source)
local it = function() return dir_it(dir) end
for item in it do
if not (item == "." or item == "..") then
local item_src_path = bg_makepath(source, item)
local item_dst_path = bg_makepath(dest, item)
local mode = lfs.attributes(item_src_path, "mode")
if mode == "directory" then
lfs.mkdir(item_dst_path)
bg_copydir(item_src_path, item_dst_path)
elseif mode == "file" then
local inf = io.open(item_src_path, "rb")
local outf = io.open(item_dst_path, "wb")
local data = inf:read("*all")
outf:write(data)
inf:close()
outf:close()
else
bg_fatal("unknown type "..item_src_path)
end
end
end
end
print("gpfault.net generator")
if #arg > 0 then
local settings = bg_parse_cmdline(arg)
bg_validate_settings(settings)
local templates = bg_load_templates(settings.src)
local template_api = {
["templates"] = templates,
["sink"] = function(tapi, str, ...)
tapi.out_file:write(str:format(...))
end,
["render"] = function(tapi, name)
tapi.templates[name](tapi)
end
}
local pages = bg_load_content(settings.src, settings.content_dirs)
template_api.pages = pages
for _, page in pairs(pages) do
local template = templates[page.template]
if not template then
bg_fatal(page.title.." uses missing template "..page.template)
end
local output_dir_path = bg_makepath(settings.dst, page.rel_out_path)
if lfs.attributes(output_dir_path, "mode") ~= "directory" then
if not lfs.mkdir(output_dir_path) then
bg_fatal("failed to create directory "..output_dir_path)
end
end
local output_file_basename = page.slug
local output_file_path = bg_makepath(output_dir_path, output_file_basename)
template_api.out_file = io.open(output_file_path, "w")
if not template_api.out_file then
bg_fatal("failed to open output file "..output_file_path)
end
template_api.data = page
template(template_api)
template_api.out_file:close()
end
for _, asset_dir in pairs(settings.asset_dirs) do
local dst_path = bg_makepath(settings.dst, "assets")
lfs.mkdir(dst_path)
bg_copydir(bg_makepath(settings.src, asset_dir), dst_path)
end
else
print([[
usage: lua.exe generator.lua --source-dir <path-to-source-folder>
--destination-dir <path-to-destination-folder>]])
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment