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.
-rw-r--r--assets/cgit.css90
-rw-r--r--assets/cgit.js161
-rw-r--r--cgitrc.5.txt17
-rwxr-xr-xextensions/html-converters/md2html304
-rwxr-xr-xextensions/html-converters/rst2html2
-rw-r--r--source/cgit.c3
-rw-r--r--source/cgit.h1
-rw-r--r--source/ui-blob.c7
-rw-r--r--source/ui-blob.h2
-rw-r--r--source/ui-summary.c48
10 files changed, 313 insertions, 322 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
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index fd420f1..b5315cc 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -31,8 +31,9 @@ about-filter::
about pages (both top-level and for each repository). The command will
get the content of the about-file on its STDIN, the name of the file
as the first argument, and the STDOUT from the command will be
- included verbatim on the about page. Default value: none. See
- also: "FILTER API".
+ included verbatim on the about page. Default value: none. When no
+ about-filter is set, a markdown readme is instead rendered by the
+ bundled cgit.js (see enable-markdown). See also: "FILTER API".
agefile::
Specifies a path, relative to each repository path, which can be used
@@ -229,6 +230,14 @@ enable-tree-group-dirs::
in git's own order, with directories and files intermixed by name.
Default value: "0".
+enable-markdown::
+ Flag which, when set to "1", renders a markdown readme on the about
+ page. cgit escapes the source and a small renderer bundled in cgit.js
+ formats it in the browser, so the page stays readable as plain text
+ when scripting is off. It applies only when no about-filter handles the
+ file, and only to names ending in .md, .markdown, .mkd or .mdown.
+ Default value: "1".
+
favicon::
Url used as link to a shortcut icon for cgit. It is suggested to use
the value "/favicon.ico" since certain browsers will ignore other
@@ -918,8 +927,8 @@ mimetype.svg=image/svg+xml
# Source code is highlighted in the browser by the bundled cgit.js, so no
# source-filter is needed here. Set one only to override that.
-# Format markdown, restructuredtext, manpages, text files, and html files
-# through the right converters
+# Markdown about pages render client-side by default (see enable-markdown),
+# so a filter is only needed for other formats such as manpages.
about-filter=/var/www/cgit/filters/about-formatting.sh
##
diff --git a/extensions/html-converters/md2html b/extensions/html-converters/md2html
deleted file mode 100755
index 59f43a8..0000000
--- a/extensions/html-converters/md2html
+++ /dev/null
@@ -1,304 +0,0 @@
-#!/usr/bin/env python3
-import markdown
-import sys
-import io
-from pygments.formatters import HtmlFormatter
-from markdown.extensions.toc import TocExtension
-sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
-sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-sys.stdout.write('''
-<style>
-.markdown-body {
- font-size: 14px;
- line-height: 1.6;
- overflow: hidden;
-}
-.markdown-body>*:first-child {
- margin-top: 0 !important;
-}
-.markdown-body>*:last-child {
- margin-bottom: 0 !important;
-}
-.markdown-body a.absent {
- color: #c00;
-}
-.markdown-body a.anchor {
- display: block;
- padding-left: 30px;
- margin-left: -30px;
- cursor: pointer;
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
-}
-.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
- margin: 20px 0 10px;
- padding: 0;
- font-weight: bold;
- -webkit-font-smoothing: antialiased;
- cursor: text;
- position: relative;
-}
-.markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link {
- display: none;
- color: #000;
-}
-.markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor {
- text-decoration: none;
- line-height: 1;
- padding-left: 0;
- margin-left: -22px;
- top: 15%;
-}
-.markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link {
- display: inline-block;
-}
-div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div#cgit .markdown-body h3 a.toclink, div#cgit .markdown-body h4 a.toclink, div#cgit .markdown-body h5 a.toclink, div#cgit .markdown-body h6 a.toclink {
- color: black;
-}
-.markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code {
- font-size: inherit;
-}
-.markdown-body h1 {
- font-size: 28px;
- color: #000;
-}
-.markdown-body h2 {
- font-size: 24px;
- border-bottom: 1px solid #ccc;
- color: #000;
-}
-.markdown-body h3 {
- font-size: 18px;
-}
-.markdown-body h4 {
- font-size: 16px;
-}
-.markdown-body h5 {
- font-size: 14px;
-}
-.markdown-body h6 {
- color: #777;
- font-size: 14px;
-}
-.markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre {
- margin: 15px 0;
-}
-.markdown-body hr {
- border: 2px solid #ccc;
-}
-.markdown-body>h2:first-child, .markdown-body>h1:first-child, .markdown-body>h1:first-child+h2, .markdown-body>h3:first-child, .markdown-body>h4:first-child, .markdown-body>h5:first-child, .markdown-body>h6:first-child {
- margin-top: 0;
- padding-top: 0;
-}
-.markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 {
- margin-top: 0;
- padding-top: 0;
-}
-.markdown-body h1+p, .markdown-body h2+p, .markdown-body h3+p, .markdown-body h4+p, .markdown-body h5+p, .markdown-body h6+p {
- margin-top: 0;
-}
-.markdown-body li p.first {
- display: inline-block;
-}
-.markdown-body ul, .markdown-body ol {
- padding-left: 30px;
-}
-.markdown-body ul.no-list, .markdown-body ol.no-list {
- list-style-type: none;
- padding: 0;
-}
-.markdown-body ul li>:first-child, .markdown-body ul li ul:first-of-type, .markdown-body ul li ol:first-of-type, .markdown-body ol li>:first-child, .markdown-body ol li ul:first-of-type, .markdown-body ol li ol:first-of-type {
- margin-top: 0px;
-}
-.markdown-body ul li p:last-of-type, .markdown-body ol li p:last-of-type {
- margin-bottom: 0;
-}
-.markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul {
- margin-bottom: 0;
-}
-.markdown-body dl {
- padding: 0;
-}
-.markdown-body dl dt {
- font-size: 14px;
- font-weight: bold;
- font-style: italic;
- padding: 0;
- margin: 15px 0 5px;
-}
-.markdown-body dl dt:first-child {
- padding: 0;
-}
-.markdown-body dl dt>:first-child {
- margin-top: 0px;
-}
-.markdown-body dl dt>:last-child {
- margin-bottom: 0px;
-}
-.markdown-body dl dd {
- margin: 0 0 15px;
- padding: 0 15px;
-}
-.markdown-body dl dd>:first-child {
- margin-top: 0px;
-}
-.markdown-body dl dd>:last-child {
- margin-bottom: 0px;
-}
-.markdown-body blockquote {
- border-left: 4px solid #DDD;
- padding: 0 15px;
- color: #777;
-}
-.markdown-body blockquote>:first-child {
- margin-top: 0px;
-}
-.markdown-body blockquote>:last-child {
- margin-bottom: 0px;
-}
-.markdown-body table th {
- font-weight: bold;
-}
-.markdown-body table th, .markdown-body table td {
- border: 1px solid #ccc;
- padding: 6px 13px;
-}
-.markdown-body table tr {
- border-top: 1px solid #ccc;
- background-color: #fff;
-}
-.markdown-body table tr:nth-child(2n) {
- background-color: #f8f8f8;
-}
-.markdown-body img {
- max-width: 100%;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.markdown-body span.frame {
- display: block;
- overflow: hidden;
-}
-.markdown-body span.frame>span {
- border: 1px solid #ddd;
- display: block;
- float: left;
- overflow: hidden;
- margin: 13px 0 0;
- padding: 7px;
- width: auto;
-}
-.markdown-body span.frame span img {
- display: block;
- float: left;
-}
-.markdown-body span.frame span span {
- clear: both;
- color: #333;
- display: block;
- padding: 5px 0 0;
-}
-.markdown-body span.align-center {
- display: block;
- overflow: hidden;
- clear: both;
-}
-.markdown-body span.align-center>span {
- display: block;
- overflow: hidden;
- margin: 13px auto 0;
- text-align: center;
-}
-.markdown-body span.align-center span img {
- margin: 0 auto;
- text-align: center;
-}
-.markdown-body span.align-right {
- display: block;
- overflow: hidden;
- clear: both;
-}
-.markdown-body span.align-right>span {
- display: block;
- overflow: hidden;
- margin: 13px 0 0;
- text-align: right;
-}
-.markdown-body span.align-right span img {
- margin: 0;
- text-align: right;
-}
-.markdown-body span.float-left {
- display: block;
- margin-right: 13px;
- overflow: hidden;
- float: left;
-}
-.markdown-body span.float-left span {
- margin: 13px 0 0;
-}
-.markdown-body span.float-right {
- display: block;
- margin-left: 13px;
- overflow: hidden;
- float: right;
-}
-.markdown-body span.float-right>span {
- display: block;
- overflow: hidden;
- margin: 13px auto 0;
- text-align: right;
-}
-.markdown-body code, .markdown-body tt {
- margin: 0 2px;
- padding: 0px 5px;
- border: 1px solid #eaeaea;
- background-color: #f8f8f8;
- border-radius: 3px;
-}
-.markdown-body code {
- white-space: nowrap;
-}
-.markdown-body pre>code {
- margin: 0;
- padding: 0;
- white-space: pre;
- border: none;
- background: transparent;
-}
-.markdown-body .highlight pre, .markdown-body pre {
- background-color: #f8f8f8;
- border: 1px solid #ccc;
- font-size: 13px;
- line-height: 19px;
- overflow: auto;
- padding: 6px 10px;
- border-radius: 3px;
-}
-.markdown-body pre code, .markdown-body pre tt {
- margin: 0;
- padding: 0;
- background-color: transparent;
- border: none;
-}
-''')
-sys.stdout.write(HtmlFormatter(style='pastie').get_style_defs('.highlight'))
-sys.stdout.write('''
-</style>
-''')
-sys.stdout.write("<div class='markdown-body'>")
-sys.stdout.flush()
-# Note: you may want to run this through bleach for sanitization
-markdown.markdownFromFile(
- output_format="html5",
- extensions=[
- "markdown.extensions.fenced_code",
- "markdown.extensions.codehilite",
- "markdown.extensions.tables",
- "markdown.extensions.sane_lists",
- TocExtension(anchorlink=True)],
- extension_configs={
- "markdown.extensions.codehilite":{"css_class":"highlight"}})
-sys.stdout.write("</div>")
diff --git a/extensions/html-converters/rst2html b/extensions/html-converters/rst2html
deleted file mode 100755
index 02d90f8..0000000
--- a/extensions/html-converters/rst2html
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/bash
-exec rst2html.py --template <(echo -e "%(stylesheet)s\n%(body_pre_docinfo)s\n%(docinfo)s\n%(body)s")
diff --git a/source/cgit.c b/source/cgit.c
index 29b2131..8a7027c 100644
--- a/source/cgit.c
+++ b/source/cgit.c
@@ -201,6 +201,8 @@ static void config_cb(const char *name, const char *value)
ctx.cfg.enable_tree_linenumbers = atoi(value);
else if (!strcmp(name, "enable-tree-group-dirs"))
ctx.cfg.enable_tree_group_dirs = atoi(value);
+ else if (!strcmp(name, "enable-markdown"))
+ ctx.cfg.enable_markdown = atoi(value);
else if (!strcmp(name, "enable-git-config"))
ctx.cfg.enable_git_config = atoi(value);
else if (!strcmp(name, "enable-cache-list"))
@@ -394,6 +396,7 @@ static void prepare_context(void)
ctx.cfg.enable_http_clone = 1;
ctx.cfg.enable_index_owner = 1;
ctx.cfg.enable_tree_linenumbers = 1;
+ ctx.cfg.enable_markdown = 1;
ctx.cfg.enable_git_config = 0;
ctx.cfg.max_repo_count = 50;
ctx.cfg.max_commit_count = 50;
diff --git a/source/cgit.h b/source/cgit.h
index ab7b284..3cd0317 100644
--- a/source/cgit.h
+++ b/source/cgit.h
@@ -243,6 +243,7 @@ struct cgit_config {
int enable_html_serving;
int enable_tree_linenumbers;
int enable_tree_group_dirs;
+ int enable_markdown;
int enable_git_config;
int enable_cache_list;
int local_time;
diff --git a/source/ui-blob.c b/source/ui-blob.c
index bc91656..7720a28 100644
--- a/source/ui-blob.c
+++ b/source/ui-blob.c
@@ -67,7 +67,7 @@ done:
return walk_tree_ctx.found_path;
}
-int cgit_print_file(char *path, const char *head, int file_only)
+int cgit_print_file(char *path, const char *head, int file_only, int html_escape)
{
struct object_id oid;
enum object_type type;
@@ -106,7 +106,10 @@ int cgit_print_file(char *path, const char *head, int file_only)
if (!buf)
return -1;
buf[size] = '\0';
- html_raw(buf, size);
+ if (html_escape)
+ html_txt(buf);
+ else
+ html_raw(buf, size);
free(buf);
return 0;
}
diff --git a/source/ui-blob.h b/source/ui-blob.h
index 16847b2..efbc94e 100644
--- a/source/ui-blob.h
+++ b/source/ui-blob.h
@@ -2,7 +2,7 @@
#define UI_BLOB_H
extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only);
-extern int cgit_print_file(char *path, const char *head, int file_only);
+extern int cgit_print_file(char *path, const char *head, int file_only, int html_escape);
extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only);
#endif /* UI_BLOB_H */
diff --git a/source/ui-summary.c b/source/ui-summary.c
index 947812a..7e533e1 100644
--- a/source/ui-summary.c
+++ b/source/ui-summary.c
@@ -99,6 +99,17 @@ static char* append_readme_path(const char *filename, const char *ref, const cha
return full_path;
}
+static int readme_is_markdown(const char *filename)
+{
+ const char *ext = strrchr(filename, '.');
+
+ if (!ext || !ext[1])
+ return 0;
+ ext++;
+ return !strcasecmp(ext, "md") || !strcasecmp(ext, "markdown") ||
+ !strcasecmp(ext, "mkd") || !strcasecmp(ext, "mdown");
+}
+
void cgit_print_repo_readme(const char *path)
{
char *filename, *ref, *mimetype;
@@ -128,16 +139,35 @@ void cgit_print_repo_readme(const char *path)
goto done;
}
- /* Print the calculated readme, either from the git repo or from the
- * filesystem, while applying the about-filter.
- */
html("<div id='summary'>");
- cgit_open_filter(ctx.repo->about_filter, filename);
- if (ref)
- cgit_print_file(filename, ref, 1);
- else
- html_include(filename);
- cgit_close_filter(ctx.repo->about_filter);
+ if (!ctx.repo->about_filter && ctx.cfg.enable_markdown &&
+ readme_is_markdown(filename)) {
+ /* No about-filter is set, so hand the markdown source to the
+ * built-in client-side renderer in cgit.js. The source is
+ * escaped here and rendered in the browser, and it degrades to
+ * readable plain text when scripting is off.
+ */
+ html("<div class='markdown' data-markdown>");
+ if (ref) {
+ cgit_print_file(filename, ref, 1, 1);
+ } else {
+ struct strbuf sb = STRBUF_INIT;
+ if (strbuf_read_file(&sb, filename, 0) >= 0)
+ html_txt(sb.buf);
+ strbuf_release(&sb);
+ }
+ html("</div>");
+ } else {
+ /* Otherwise print the readme through the about-filter, or raw
+ * when none is configured.
+ */
+ cgit_open_filter(ctx.repo->about_filter, filename);
+ if (ref)
+ cgit_print_file(filename, ref, 1, 0);
+ else
+ html_include(filename);
+ cgit_close_filter(ctx.repo->about_filter);
+ }
html("</div>");
if (free_filename)