#!/usr/bin/env lua
--
-- lume-docs-build.lua
-- Converts Lume's Markdown documentation into a standalone HTML site.
-- Usage: lua lume-docs-build.lua [input.md] [output.html]
-- Defaults: DOCS.md -> docs/index.html
--
local infile = arg and arg[1] or "DOCS.md"
local outfile = arg and arg[2] or "index.html"
-- ── Utilities ────────────────────────────────────────────────────────────────
local function read_file(path)
local f, err = io.open(path, "r")
if not f then
io.stderr:write("lume-docs-build: cannot open '" .. path .. "': " .. tostring(err) .. "\n")
os.exit(1)
end
local content = f:read("*a")
f:close()
return content
end
local function write_file(path, content)
-- create parent directory if it doesn't exist (best-effort)
local dir = path:match("^(.*)/[^/]+$")
if dir and dir ~= "" then os.execute("mkdir -p " .. dir) end
local f, err = io.open(path, "w")
if not f then
io.stderr:write("lume-docs-build: cannot write '" .. path .. "': " .. tostring(err) .. "\n")
os.exit(1)
end
f:write(content)
f:close()
end
local function escape_html(s)
return (s:gsub("&", "&")
:gsub("<", "<")
:gsub(">", ">")
:gsub('"', """))
end
-- ── Slug generation (for heading anchors) ────────────────────────────────────
local function slugify(text)
-- strip markdown inline code and links from heading text
text = text:gsub("`[^`]+`", function(m) return m:sub(2,-2) end)
text = text:gsub("%[(.-)%]%(.-%)"," %1")
text = text:lower()
text = text:gsub("[^%a%d%s%-]", "")
text = text:gsub("%s+", "-")
text = text:gsub("^%-+", ""):gsub("%-+$", "")
return text
end
-- ── Inline Markdown → HTML ────────────────────────────────────────────────────
-- Handles: **bold**, *italic*, `code`, [text](url), and bare anchors
local function inline_md(s)
-- escape HTML first so we don't double-escape our own tags
s = escape_html(s)
-- code spans (backtick) — protect from further processing
local code_stash = {}
s = s:gsub("`([^`]+)`", function(c)
code_stash[#code_stash+1] = '' .. c .. ""
return "\x00CODE" .. #code_stash .. "\x00"
end)
-- bold + italic ***text***
s = s:gsub("%*%*%*(.-)%*%*%*", "%1")
-- bold **text**
s = s:gsub("%*%*(.-)%*%*", "%1")
-- italic *text*
s = s:gsub("%*(.-)%*", "%1")
-- links [text](url)
s = s:gsub("%[(.-)%]%((.-)%)", function(txt, url)
-- internal anchor links start with #
if url:sub(1,1) == "#" then
return '' .. txt .. ""
end
return '' .. txt .. ""
end)
-- restore code spans
s = s:gsub("\x00CODE(%d+)\x00", function(n)
return code_stash[tonumber(n)]
end)
return s
end
-- ── Syntax highlighting for code blocks ──────────────────────────────────────
-- Simple but good-enough token colouring for lua, c, sh, and text.
local SH_KEYWORDS = {
["if"]=1,["then"]=1,["else"]=1,["elif"]=1,["fi"]=1,
["for"]=1,["while"]=1,["do"]=1,["done"]=1,["case"]=1,
["esac"]=1,["in"]=1,["function"]=1,["return"]=1,
["local"]=1,["export"]=1,["echo"]=1,["exit"]=1,
}
local LUA_KEYWORDS = {
["and"]=1,["break"]=1,["do"]=1,["else"]=1,["elseif"]=1,
["end"]=1,["false"]=1,["for"]=1,["function"]=1,["goto"]=1,
["if"]=1,["in"]=1,["local"]=1,["nil"]=1,["not"]=1,
["or"]=1,["repeat"]=1,["return"]=1,["then"]=1,["true"]=1,
["until"]=1,["while"]=1,
}
local C_KEYWORDS = {
["auto"]=1,["break"]=1,["case"]=1,["char"]=1,["const"]=1,
["continue"]=1,["default"]=1,["do"]=1,["double"]=1,["else"]=1,
["enum"]=1,["extern"]=1,["float"]=1,["for"]=1,["goto"]=1,
["if"]=1,["inline"]=1,["int"]=1,["long"]=1,["register"]=1,
["return"]=1,["short"]=1,["signed"]=1,["sizeof"]=1,["static"]=1,
["struct"]=1,["switch"]=1,["typedef"]=1,["union"]=1,["unsigned"]=1,
["void"]=1,["volatile"]=1,["while"]=1,["NULL"]=1,["true"]=1,["false"]=1,
}
local function hl_lua(code)
local out = {}
local i, n = 1, #code
while i <= n do
local ch = code:sub(i,i)
-- single-line comment
if code:sub(i,i+1) == "--" then
local rest = escape_html(code:sub(i))
out[#out+1] = '' .. rest .. ""
break
-- long string / comment stubs not handled — treated as unknown
elseif ch == '"' or ch == "'" then
local q, j = ch, i+1
while j <= n do
local c = code:sub(j,j)
if c == "\\" then j=j+2 elseif c==q then j=j+1; break else j=j+1 end
end
out[#out+1] = ''..escape_html(code:sub(i,j-1))..""
i=j
elseif ch:match("%d") then
local j=i
while j<=n and code:sub(j,j):match("[%d%.xXa-fA-F]") do j=j+1 end
out[#out+1]=''..escape_html(code:sub(i,j-1))..""
i=j
elseif ch:match("[%a_]") then
local j=i
while j<=n and code:sub(j,j):match("[%w_]") do j=j+1 end
local word=code:sub(i,j-1)
if LUA_KEYWORDS[word] then
out[#out+1]=''..word..""
else
out[#out+1]=escape_html(word)
end
i=j
else
out[#out+1]=escape_html(ch); i=i+1
end
end
return table.concat(out)
end
local function hl_c(code)
local out = {}
local i, n = 1, #code
while i <= n do
local ch = code:sub(i,i)
if code:sub(i,i+1) == "//" then
out[#out+1]=''..escape_html(code:sub(i))..""; break
elseif ch == '"' or ch == "'" then
local q,j=ch,i+1
while j<=n do
local c=code:sub(j,j)
if c=="\\" then j=j+2 elseif c==q then j=j+1;break else j=j+1 end
end
out[#out+1]=''..escape_html(code:sub(i,j-1))..""; i=j
elseif ch:match("%d") then
local j=i
while j<=n and code:sub(j,j):match("[%d%.xXuUlL]") do j=j+1 end
out[#out+1]=''..escape_html(code:sub(i,j-1))..""; i=j
elseif ch:match("[%a_]") then
local j=i
while j<=n and code:sub(j,j):match("[%w_]") do j=j+1 end
local word=code:sub(i,j-1)
if C_KEYWORDS[word] then
out[#out+1]=''..word..""
else
out[#out+1]=escape_html(word)
end
i=j
else
out[#out+1]=escape_html(ch); i=i+1
end
end
return table.concat(out)
end
local function hl_sh(code)
local out = {}
local i,n=1,#code
while i<=n do
local ch=code:sub(i,i)
if ch=="#" then
out[#out+1]=''..escape_html(code:sub(i))..""; break
elseif ch=='"' or ch=="'" then
local q,j=ch,i+1
while j<=n do
local c=code:sub(j,j)
if ch=='"' and c=="\\" then j=j+2 elseif c==q then j=j+1;break else j=j+1 end
end
out[#out+1]=''..escape_html(code:sub(i,j-1))..""; i=j
elseif ch=="$" then
out[#out+1]='$'; i=i+1
elseif ch:match("[%a_]") then
local j=i
while j<=n and code:sub(j,j):match("[%w_]") do j=j+1 end
local word=code:sub(i,j-1)
if SH_KEYWORDS[word] then
out[#out+1]=''..word..""
else
out[#out+1]=escape_html(word)
end
i=j
else
out[#out+1]=escape_html(ch); i=i+1
end
end
return table.concat(out)
end
local highlighters = { lua=hl_lua, c=hl_c, sh=hl_sh, bash=hl_sh, zsh=hl_sh }
local function highlight_code(lang, code)
local hl = lang and highlighters[lang:lower()]
if hl then
-- highlight line by line
local lines = {}
for line in (code .. "\n"):gmatch("([^\n]*)\n") do
lines[#lines+1] = hl(line)
end
return table.concat(lines, "\n")
end
return escape_html(code)
end
-- ── Block-level Markdown parser ───────────────────────────────────────────────
local function parse_md(src)
local lines = {}
for ln in (src .. "\n"):gmatch("([^\n]*)\n") do
lines[#lines+1] = ln
end
local out = {}
local toc = {} -- {level, text, slug}
local i = 1
local in_list = false
local in_para = false
local function close_para()
if in_para then out[#out+1] = "
'
.. body
.. "" goto continue end -- table | col | col | if line:match("^|") then close_all() local tbl_lines = {} while i <= #lines and lines[i]:match("^|") do tbl_lines[#tbl_lines+1] = lines[i] i = i + 1 end out[#out+1] = '" .. inline_md(table.concat(bq_lines, " ")) .. "
| " .. inline_md(cell:match("^%s*(.-)%s*$")) .. " | " end out[#out+1] = "
|---|
| " .. inline_md(cell:match("^%s*(.-)%s*$")) .. " | " end out[#out+1] = "
" in_para = true else out[#out+1] = " " end out[#out+1] = inline_md(line) i = i + 1 ::continue:: end close_all() return table.concat(out, "\n"), toc end -- ── TOC sidebar HTML ────────────────────────────────────────────────────────── local function build_sidebar(toc) local out = {} out[#out+1] = '' return table.concat(out, "\n") end -- ── Full HTML template ──────────────────────────────────────────────────────── local function build_html(title, sidebar_html, body_html) return string.format([[