-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[[ HTML rendering for lilgit: page shell, views, escaping. `http.html_escape` escapes only `<`/`>` and is not attribute-safe, so lilgit ships its own full escaper. Page templates are `${var}` strings filled with `std.txt.template`; users can override them via the `templates` config table. ]] local std = require("std") local url = require("http.url") local markdown = require("markdown") -- The `&` substitution must run first, or it would re-escape the others. local html_escape = function(s) return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """):gsub("'", "'")) end --[[ Build a browse URL: `/repo/kind/ref/path...`. The ref may contain `/` (slashed branches), so it is split and every segment escaped individually. `trailing` appends the canonical trailing slash for tree URLs. ]] local href = function(repo_name, kind, ref_str, path_segs, trailing) local parts = { "", url.escape(repo_name), kind } for seg in ref_str:gmatch("[^/]+") do parts[#parts + 1] = url.escape(seg) end for _, seg in ipairs(path_segs or {}) do parts[#parts + 1] = url.escape(seg) end local s = table.concat(parts, "/") if trailing then s = s .. "/" end return s end --[[ Render markdown/djot to an HTML fragment. The first `

` is extracted as the page title and removed from the body (the page shell displays the title itself); on parse failure the content is served as escaped `
`. Returns `html, title` with the filename as the title fallback.
]]
local render_markdown = function(content, filename)
	local ok, html = pcall(markdown.render_html, content)
	if not ok or not html then
		return "
" .. html_escape(content) .. "
", filename end local title local h1 = html:match("]*>(.-)

") if h1 then title = h1:gsub("<[^>]+>", ""):gsub("^%s+", ""):gsub("%s+$", "") html = html:gsub("]*>.-%s*", "", 1) end if not title or title == "" then title = filename end return html, title end local default_templates = { page = [[ ${title} — ${site_title}
${body}
]], } local build_templates = function(cfg) local templates = std.tbl.copy(default_templates) return std.tbl.merge(templates, cfg.templates or {}) end local page = function(state, title, nav, body) return std.txt.template(state.templates.page, { site_title = html_escape(state.cfg.site_title), title = html_escape(title), nav = nav, body = body, }) end local index_nav = function(state) return '' .. html_escape(state.cfg.site_title) .. "" end local repo_nav = function(state, repo, ref_str) local nav = index_nav(state) .. ' / ' .. html_escape(repo.name) .. "" if ref_str and ref_str ~= repo.default_branch then nav = nav .. ' / ' .. html_escape(ref_str) .. "" end return nav end local index_page = function(state, host) local scheme = state.cfg.ssl and "https" or "http" local rows = {} for _, repo in ipairs(state.registry.ordered) do local clone_url = scheme .. "://" .. (host or "localhost") .. "/" .. url.escape(repo.name) rows[#rows + 1] = '' .. html_escape(repo.name) .. "" .. html_escape(repo.description or "") .. "" .. html_escape(clone_url) .. "" end local body if #rows == 0 then body = "

No repositories.

" else body = '\n' .. table.concat(rows, "\n") .. "\n
namedescriptionclone
" end return page(state, state.cfg.site_title, index_nav(state), body) end local KIND_MARKERS = { tree = "d", blob = "-", symlink = "l", gitlink = "@" } local tree_page = function(state, repo, ref_str, path_segs, listing, readme_html, readme_title) local title = repo.name if #path_segs > 0 then title = repo.name .. "/" .. table.concat(path_segs, "/") end local rows = {} if #path_segs > 0 then local parent = {} for i = 1, #path_segs - 1 do parent[i] = path_segs[i] end rows[#rows + 1] = 'd..' end for _, entry in ipairs(listing) do local child = {} for i, seg in ipairs(path_segs) do child[i] = seg end child[#child + 1] = entry.name local cell if entry.kind == "tree" then cell = '' .. html_escape(entry.name) .. "/" elseif entry.kind == "gitlink" then cell = '' .. html_escape(entry.name) .. " @ " .. entry.sha:sub(1, 7) .. "" else cell = '' .. html_escape(entry.name) .. "" end rows[#rows + 1] = '' .. (KIND_MARKERS[entry.kind] or "-") .. '' .. cell .. "" end local body = "

" .. html_escape(title) .. '

\n\n' .. table.concat(rows, "\n") .. "\n
" if readme_html then body = body .. '\n
\n' if readme_title then body = body .. "

" .. html_escape(readme_title) .. "

\n" end body = body .. readme_html .. "\n
" end return page(state, title, repo_nav(state, repo, ref_str), body) end local blob_page = function(state, repo, ref_str, path_segs, body_html, title) local raw = 'raw' local body = raw .. "\n

" .. html_escape(title) .. "

\n" .. body_html return page(state, title, repo_nav(state, repo, ref_str), body) end return { html_escape = html_escape, href = href, markdown = render_markdown, build_templates = build_templates, index_page = index_page, tree_page = tree_page, blob_page = blob_page, }