local function resolve(base, relative) if relative:sub(1, 1) == "/" then return relative end local combined = base .. "/" .. relative local segments = {} for segment in combined:gmatch("[^/]+") do if segment == ".." then if #segments > 0 and segments[#segments] ~= ".." then table.remove(segments) else table.insert(segments, "..") end elseif segment ~= "." and segment ~= "" then table.insert(segments, segment) end end local prefix = (base:sub(1, 1) == "/" or relative:sub(1, 1) == "/") and "/" or "" return prefix .. table.concat(segments, "/") end -- The branch from the query string (?h=...), or nil. cgit doesn't pass it -- to filters, but Lua filters run in cgit's process, which has the CGI -- environment. local function query_head() local query = os.getenv("QUERY_STRING") or "" for key, value in query:gmatch("([^&=]+)=([^&]*)") do if key == "h" then value = value:gsub("%+", " "):gsub("%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end) if value ~= "" then return value end end end return nil end local function url_escape(s) return (s:gsub("[^%w._~/-]", function(c) return string.format("%%%02X", c:byte()) end)) end -- cgit provides html() to Lua filters. It writes in order with cgit's own -- output, which io.write's buffered stdout doesn't. io.write is for running -- the filter outside of cgit. local emit = html or io.write -- Run md2html and return its output. local function md2html(markdown) local input = os.tmpname() local file = assert(io.open(input, "w")) file:write(markdown) file:close() local handle = io.popen("@md2html@ --github < " .. input, "r") local output = handle and handle:read("*a") if handle then handle:close() end os.remove(input) return output end local table_style = [[ ]] -- Column widths, shared by all tables with the same header. A column's -- weight is the larger of its longest word and its average cell length, in -- characters; code counts more, as monospace is wider. Narrow columns get a -- fixed width, the others a share of what's left. local code_weight = 1.25 local narrow = 12 local function length(s) s = s:gsub("&#?%w+;", "x") return select(2, s:gsub("[^\128-\191]", "")) end local function cell_weights(cell) local longest, total = 0, 0 local function add(text, factor) for word in text:gmatch("%S+") do local n = length(word) * factor longest = math.max(longest, n) total = total + n + 1 end end for code in cell:gmatch("(.-)") do add((code:gsub("<[^>]+>", "")), code_weight) end add((cell:gsub(".-", " "):gsub("<[^>]+>", " ")), 1) return longest, math.max(total - 1, 0) end local function cells(row) local result = {} for cell in row:gmatch("]*>(.-)") do table.insert(result, cell) end return result end local function header(tbl) return table.concat(cells(tbl:match("(.-)") or ""), "\0") end local function column_widths(output) local stats = {} for tbl in output:gmatch(".-
") do local key = header(tbl) stats[key] = stats[key] or { rows = 0, longest = {}, total = {} } local s = stats[key] for row in tbl:gmatch("(.-)") do s.rows = s.rows + 1 for i, cell in ipairs(cells(row)) do local longest, total = cell_weights(cell) s.longest[i] = math.max(s.longest[i] or 0, longest) s.total[i] = (s.total[i] or 0) + total end end end local widths = {} for key, s in pairs(stats) do local weights, wide = {}, 0 for i = 1, #s.longest do weights[i] = math.max(s.longest[i], s.total[i] / s.rows) if weights[i] > narrow then wide = wide + weights[i] end end local cols = {} for i, weight in ipairs(weights) do if weight > narrow then cols[i] = string.format('', 100 * weight / wide) else cols[i] = string.format('', math.ceil(weight) + 1) end end widths[key] = "" .. table.concat(cols) .. "" end return widths end -- Add the column widths, and let code break after underscores. local function layout_tables(output) local widths = column_widths(output) return (output:gsub(".-
", function(tbl) local key = header(tbl) tbl = tbl:gsub("(.-)", function(code) return "" .. code:gsub("_", "_") .. "" end) return "" .. widths[key] .. tbl:sub(#"
" + 1) end)) end local buffer = "" local should_render = true local current_dir = "" -- Stores the directory prefix of the file being processed local current_file = "" function filter_open(...) local args = {...} current_file = args[1] or "" if current_file == "" then current_file = "README.md" end -- Strict route enforcement: Allow README.md OR any file starting with docs/ and ending in .md if current_file == "README.md" or (current_file:sub(1, 5) == "docs/" and current_file:sub(-3) == ".md") then should_render = true -- Extract directory context (e.g., "docs/ops.md" -> "docs/") -- If it's the root README.md, current_dir stays an empty string "" if current_file ~= "README.md" then current_dir = current_file:match("(.*/)") or "" else current_dir = "" end else should_render = false end buffer = "" end function filter_write(str) if should_render then buffer = buffer .. str end end function filter_close() if not should_render then emit("

Error: Access Denied. Only README.md and documentation markdown files are viewable here.

") return end local repo = os.getenv("CGIT_REPO_URL") or "" local head = query_head() local query = "" if head and head ~= os.getenv("CGIT_REPO_DEFBRANCH") then query = "?h=" .. url_escape(head) end local processed = buffer:gsub("%[([^%]]+)%]%(([^:)]+)%)", function(text, path) -- Ignore protocols (http, https, etc.), absolute paths (/), and anchors (#) if path:sub(1, 1) == "/" or path:sub(1, 1) == "#" or path:match("^%w+:") then return string.format("[%s](%s)", text, path) end local fragment path, fragment = path:match("^([^#]*)(.*)$") path = resolve(current_dir, path) if path:sub(-3) == ".md" then return string.format("[%s](/%s/about/%s%s%s)", text, repo, path, query, fragment) else return string.format("[%s](/%s/tree/%s%s%s)", text, repo, path, query, fragment) end end) local output = md2html(processed) if output then if output:find("
", 1, true) then emit(table_style) output = layout_tables(output) end emit(output) else emit("

Error: Could not execute md2html.

") end end