From 5104f90310a2028e50667ea12d9e75e820fbbd36 Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Sat, 25 Jul 2026 15:03:48 -1000 Subject: Replace the browser markdown renderer with a filter The readme is now escaped plain text unless `about-filter` points at the new `about-render.lua`, which renders markdown, man pages and plain text server-side. `enable-markdown` goes away with the renderer. --- assets/cgit.css | 8 --- assets/cgit.js | 164 -------------------------------------------------------- 2 files changed, 172 deletions(-) (limited to 'assets') diff --git a/assets/cgit.css b/assets/cgit.css index 3255068..ba7d7b1 100644 --- a/assets/cgit.css +++ b/assets/cgit.css @@ -520,14 +520,6 @@ div#cgit pre.plaintext { margin: 0; } -/* Markdown source before the client-side renderer has run, and for good - * when scripting is off. Keeps the line structure so it reads as text. */ -div#cgit .markdown[data-markdown] { - white-space: pre-wrap; - overflow-wrap: anywhere; - font-family: var(--font-mono); -} - div#cgit .markdown { line-height: 1.6; overflow-wrap: break-word; diff --git a/assets/cgit.js b/assets/cgit.js index 10a34cd..6fe53ba 100644 --- a/assets/cgit.js +++ b/assets/cgit.js @@ -194,167 +194,3 @@ document.addEventListener("DOMContentLoaded", function () { }, false); })(); - -/* Built-in Markdown rendering for the about page. When no about-filter is - * configured, cgit escapes a markdown readme into a data-markdown container - * (see cgit_print_repo_readme) and this renders a deliberately small, safe - * subset client-side: headings, lists, blockquotes, rules, fenced and inline - * code, pipe tables, links, images and emphasis. Every run of text is escaped - * before any markup is added, and link and image URLs are restricted to http, - * https, mailto and relative targets, so a hostile readme cannot inject markup - * or scripts. Fenced code carries its language in data-lang as a styling - * hook. Without JavaScript the escaped source stays readable as plain text. - * - * This is intentionally a subset, not CommonMark: no reference links, raw HTML - * passthrough, nested lists or setext headings. Configure an about-filter to - * replace it, or set enable-markdown=0 to turn it off. */ - -(function () { - -var MAX_BYTES = 400000; - -function esc(s) { - return s.replace(/&/g, "&").replace(//g, ">"); -} - -function escAttr(s) { - return esc(s).replace(/"/g, """).replace(/'/g, "'"); -} - -/* Return the url if its scheme is safe, else "". Whitespace and control bytes - * are stripped before the scheme is read because browsers ignore them when - * resolving it, so "java\nscript:..." must still be caught as javascript. */ -function safeUrl(url) { - url = (url || "").replace(/[\u0000-\u0020]+/g, ""); - var scheme = /^([a-z][a-z0-9+.\-]*):/i.exec(url); - if (scheme && !/^(https?|mailto)$/i.test(scheme[1])) - return ""; - return url; -} - -function link(text, url, image) { - var u = safeUrl(url); - if (!u) - return image ? esc("![" + text + "]") : inline(text); - if (image) - return "" + escAttr(text) + ""; - return "" + inline(text) + ""; -} - -/* Inline rendering over one block of text. Scans to the next marker character - * and bulk-escapes the plain text in between, so it stays roughly linear. */ -function inline(s) { - var out = "", i = 0, n = s.length, marker = /[`!\[*_]/g, m, rest; - while (i < n) { - marker.lastIndex = i; - m = marker.exec(s); - if (!m) { out += esc(s.slice(i)); break; } - if (m.index > i) { out += esc(s.slice(i, m.index)); i = m.index; } - rest = s.slice(i); - if ((m = /^`([^`]+)`/.exec(rest))) - out += "" + esc(m[1]) + ""; - else if ((m = /^!\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest))) - out += link(m[1], m[2], true); - else if ((m = /^\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest))) - out += link(m[1], m[2], false); - else if ((m = /^(\*\*|__)([\s\S]+?)\1/.exec(rest))) - out += "" + inline(m[2]) + ""; - else if ((m = /^(\*|_)([^\s][\s\S]*?)\1/.exec(rest))) - out += "" + inline(m[2]) + ""; - else { out += esc(s.charAt(i)); i++; continue; } - i += m[0].length; - } - return out; -} - -function cells(row) { - return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map(function (c) { - return c.trim(); - }); -} - -function render(src) { - var lines = src.replace(/\r\n?/g, "\n").split("\n"); - var out = "", i = 0, n = lines.length, line, m, k; - while (i < n) { - line = lines[i]; - if (/^\s*$/.test(line)) { i++; continue; } - if ((m = /^\s*(`{3,}|~{3,})\s*([\w.+#-]*)/.exec(line))) { - var fence = m[1].charAt(0) === "`" ? /^\s*`{3,}\s*$/ : /^\s*~{3,}\s*$/; - var lang = m[2], code = ""; - for (i++; i < n && !fence.test(lines[i]); i++) - code += lines[i] + "\n"; - i++; - out += "
" + esc(code) + "
"; - continue; - } - if ((m = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line))) { - k = m[1].length; - out += "" + inline(m[2]) + ""; - i++; continue; - } - if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { out += "
"; i++; continue; } - if (/^\s*>/.test(line)) { - var q = ""; - for (; i < n && /^\s*>/.test(lines[i]); i++) - q += lines[i].replace(/^\s*>\s?/, "") + "\n"; - out += "
" + render(q) + "
"; - continue; - } - if (line.indexOf("|") >= 0 && i + 1 < n && - /^\s*\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$/.test(lines[i + 1])) { - var head = cells(line), t = ""; - for (k = 0; k < head.length; k++) - t += ""; - t += ""; - for (i += 2; i < n && lines[i].indexOf("|") >= 0 && !/^\s*$/.test(lines[i]); i++) { - var row = cells(lines[i]); - t += ""; - for (k = 0; k < row.length; k++) - t += ""; - t += ""; - } - out += t + "
" + inline(head[k]) + "
" + inline(row[k]) + "
"; - continue; - } - if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) { - var ordered = /^\s*\d/.test(line), tag = ordered ? "ol" : "ul"; - out += "<" + tag + ">"; - for (; i < n && (m = /^\s*([-*+]|\d+[.)])\s+(.*)$/.exec(lines[i])); i++) { - if ((/\d/.test(m[1])) !== ordered) break; - out += "
  • " + inline(m[2]) + "
  • "; - } - out += ""; - continue; - } - /* Always consume the current line so i advances even when it - * matched none of the block branches above. */ - var para = lines[i++]; - for (; i < n && !/^\s*$/.test(lines[i]) && - !/^\s*(#{1,6}\s|>|`{3,}|~{3,}|([-*+]|\d+[.)])\s)/.test(lines[i]); i++) - para += "\n" + lines[i]; - out += "

    " + inline(para).replace(/\n/g, "
    ") + "

    "; - } - return out; -} - -document.addEventListener("DOMContentLoaded", function () { - var nodes = document.querySelectorAll("div#cgit [data-markdown]"), i, el, text; - for (i = 0; i < nodes.length; i++) { - el = nodes[i]; - text = el.textContent; - if (!text || text.length > MAX_BYTES) - continue; - try { - el.innerHTML = render(text); - /* The attribute doubles as the style hook for the - * unrendered source, so drop it once rendered. */ - el.removeAttribute("data-markdown"); - } catch (e) { - /* leave the escaped source in place on any failure */ - } - } -}, false); - -})(); -- cgit v2.8.0