diff options
context:
space:
mode:
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Render README markdown in the browser
cgit had no markdown support of its own, so a readme was rendered through an external python filter or not at all. Escaping the source and formatting it in cgit.js keeps the work in the browser like the blob highlighter, and the page stays readable as plain text without scripting.
Diffstat (limited to '')
-rw-r--r--assets/cgit.css90
-rw-r--r--assets/cgit.js161
2 files changed, 251 insertions, 0 deletions
diff --git a/assets/cgit.css b/assets/cgit.css
index 96192e4..10fcf66 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -496,6 +496,96 @@ div#cgit div#summary pre {
overflow-x: auto;
}
+div#cgit .markdown {
+ line-height: 1.6;
+ overflow-wrap: break-word;
+}
+
+div#cgit .markdown > :first-child {
+ margin-top: 0;
+}
+
+div#cgit .markdown > :last-child {
+ margin-bottom: 0;
+}
+
+div#cgit .markdown h1,
+div#cgit .markdown h2,
+div#cgit .markdown h3,
+div#cgit .markdown h4,
+div#cgit .markdown h5,
+div#cgit .markdown h6 {
+ margin: 1.2em 0 0.5em;
+ font-weight: bold;
+ line-height: 1.25;
+}
+
+div#cgit .markdown h1 { font-size: 1.6em; }
+div#cgit .markdown h2 {
+ font-size: 1.35em;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.2em;
+}
+div#cgit .markdown h3 { font-size: 1.15em; }
+div#cgit .markdown h4 { font-size: 1em; }
+div#cgit .markdown h5,
+div#cgit .markdown h6 { font-size: 0.9em; color: var(--muted); }
+
+div#cgit .markdown p { margin: 0.7em 0; }
+div#cgit .markdown a { color: var(--link); }
+
+div#cgit .markdown ul,
+div#cgit .markdown ol { margin: 0.7em 0; padding-left: 2em; }
+div#cgit .markdown li { margin: 0.2em 0; }
+
+div#cgit .markdown blockquote {
+ margin: 0.7em 0;
+ padding: 0.1em 1em;
+ color: var(--muted);
+ border-left: 3px solid var(--border-mid);
+}
+
+div#cgit .markdown hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 1.2em 0;
+}
+
+div#cgit .markdown code {
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ padding: 0.1em 0.35em;
+ font-size: 0.95em;
+}
+
+div#cgit .markdown pre {
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 0.7em 0.9em;
+ overflow-x: auto;
+}
+
+div#cgit .markdown pre code {
+ background: none;
+ border: none;
+ padding: 0;
+}
+
+div#cgit .markdown table {
+ border-collapse: collapse;
+ margin: 0.7em 0;
+}
+
+div#cgit .markdown th,
+div#cgit .markdown td {
+ border: 1px solid var(--border);
+ padding: 0.35em 0.7em;
+}
+
+div#cgit .markdown th { background: var(--surface-2); }
+
div#cgit table#downloads {
float: right;
border-collapse: collapse;
diff --git a/assets/cgit.js b/assets/cgit.js
index cda167e..4a0d088 100644
--- a/assets/cgit.js
+++ b/assets/cgit.js
@@ -123,6 +123,167 @@ document.addEventListener("DOMContentLoaded", function () {
})();
+/* 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 keeps its data-lang so the highlighter below styles
+ * it. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+}
+
+function escAttr(s) {
+ return esc(s).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
+}
+
+/* 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 "<img src='" + escAttr(u) + "' alt='" + escAttr(text) + "'/>";
+ return "<a href='" + escAttr(u) + "'>" + inline(text) + "</a>";
+}
+
+/* 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 += "<code>" + esc(m[1]) + "</code>";
+ 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 += "<strong>" + inline(m[2]) + "</strong>";
+ else if ((m = /^(\*|_)([^\s][\s\S]*?)\1/.exec(rest)))
+ out += "<em>" + inline(m[2]) + "</em>";
+ 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 += "<pre><code" + (lang ? " data-lang='" + escAttr(lang) + "'" : "") +
+ ">" + esc(code) + "</code></pre>";
+ continue;
+ }
+ if ((m = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line))) {
+ k = m[1].length;
+ out += "<h" + k + ">" + inline(m[2]) + "</h" + k + ">";
+ i++; continue;
+ }
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { out += "<hr/>"; i++; continue; }
+ if (/^\s*>/.test(line)) {
+ var q = "";
+ for (; i < n && /^\s*>/.test(lines[i]); i++)
+ q += lines[i].replace(/^\s*>\s?/, "") + "\n";
+ out += "<blockquote>" + render(q) + "</blockquote>";
+ continue;
+ }
+ if (line.indexOf("|") >= 0 && i + 1 < n &&
+ /^\s*\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$/.test(lines[i + 1])) {
+ var head = cells(line), t = "<table><thead><tr>";
+ for (k = 0; k < head.length; k++)
+ t += "<th>" + inline(head[k]) + "</th>";
+ t += "</tr></thead><tbody>";
+ for (i += 2; i < n && lines[i].indexOf("|") >= 0 && !/^\s*$/.test(lines[i]); i++) {
+ var row = cells(lines[i]);
+ t += "<tr>";
+ for (k = 0; k < row.length; k++)
+ t += "<td>" + inline(row[k]) + "</td>";
+ t += "</tr>";
+ }
+ out += t + "</tbody></table>";
+ 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 += "<li>" + inline(m[2]) + "</li>";
+ }
+ out += "</" + tag + ">";
+ 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 += "<p>" + inline(para).replace(/\n/g, "<br/>") + "</p>";
+ }
+ 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);
+ } catch (e) {
+ /* leave the escaped source in place on any failure */
+ }
+ }
+}, false);
+
+})();
+
/* Built-in syntax highlighting for the blob view. When no server-side
* source filter is configured, cgit tags the <code> element with
* data-lang set to the file's extension (or bare name, so Makefile and