diff options
context:
space:
mode:
-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)