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 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 emit(output) else emit("Error: Could not execute md2html.
") end end