diff options
| author | Bryce Kwon <bryce@brycekwon.com> | |
|---|---|---|
| committer | Bryce 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.js | 161 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 file changed, 161 insertions, 0 deletions
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, "&").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 "<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 |
