Download raw body.
gotwebd: render README.md as HTML, like GitHub does
Hi,
Right now gotwebd already finds README/README.md/README.txt <http://README.md/README.txt> in arepo's top-level tree (got_output_repo_tree() in got_operations.c)and shows the first match under the file listing on the tree page.The problem is that it's dumped through tp_write_htmlescape() into a<pre> block, so Markdown syntax (#, **, links, lists, etc.) shows upliterally instead of being rendered — unlike GitHub, GitLab, Gitea,or even cgit's "about" page.
This patch adds got_render_readme_markdown(), a small wrapper aroundlowdown_buf(3) from textproc/lowdown (already a port, ISC/BSDlicensed, no external dependencies, and explicitly designed to workunder pledge(2) per lowdown(3) — it doesn't need any promise beyondwhat gotwebd's fcgi process already has). Only files ending in .mdgo through this path; plain README and README.txt keep being shownexactly as before, escaped inside <pre>.
Since README content comes from repository data and not from thegotwebd operator, LOWDOWN_HTML_OWASP is enabled so any raw HTMLembedded in the Markdown gets sanitized rather than passed throughas-is.
Also adds a small .readme block to gotweb.css so headings, codeblocks, tables and blockquotes inside the rendered README don't lookout of place next to the rest of the page.
Thanks
diff --git a/gotwebd/files/htdocs/gotwebd/gotweb.css b/gotwebd/files/htdocs/gotwebd/gotweb.css
index de0fc45..4b0ae1d 100755
--- a/gotwebd/files/htdocs/gotwebd/gotweb.css
+++ b/gotwebd/files/htdocs/gotwebd/gotweb.css
@@ -382,6 +382,38 @@ header.subtitle h2 {
margin-bottom: 20px;
border-collapse: collapse;
}
+.readme {
+ max-width: 900px;
+ line-height: 1.5;
+ padding: 1em 0;
+}
+.readme h1, .readme h2, .readme h3 {
+ border-bottom: 1px solid LightSlateGray;
+ padding-bottom: 0.2em;
+}
+.readme pre {
+ background-color: #f6f6f6;
+ padding: 0.5em;
+ overflow-x: auto;
+}
+.readme code {
+ font-family: monospace;
+ background-color: #f6f6f6;
+ padding: 0.1em 0.3em;
+}
+.readme table {
+ border-collapse: collapse;
+}
+.readme th, .readme td {
+ border: 1px solid LightSlateGray;
+ padding: 0.3em 0.6em;
+}
+.readme blockquote {
+ border-left: 3px solid LightSlateGray;
+ margin-left: 0;
+ padding-left: 1em;
+ color: #555;
+}
.tree_wrapper:nth-child(odd) {
background-color: #d8f3ef;
}
diff --git a/gotwebd/got_operations.c b/gotwebd/got_operations.c
index 68e9ef0..5f0ac3a 100644
--- a/gotwebd/got_operations.c
+++ b/gotwebd/got_operations.c
@@ -28,6 +28,8 @@
#include <string.h>
#include <unistd.h>
+#include <lowdown.h>
+
#include "got_error.h"
#include "got_object.h"
#include "got_reference.h"
@@ -760,6 +762,62 @@ got_output_repo_tree(struct request *c, char **readme,
return 0;
}
+/*
+ * Render a Markdown README (buf/len) into a sanitized HTML5 fragment,
+ * the same way GitHub renders README.md on a repository's front page.
+ * Returns 0 and a NUL-terminated *html on success, -1 on failure.
+ * The caller must free *html.
+ */
+int
+got_render_readme_markdown(char **html, const uint8_t *buf, size_t len)
+{
+ struct lowdown_opts opts;
+ char *out = NULL;
+ size_t outlen = 0;
+
+ *html = NULL;
+
+ memset(&opts, 0, sizeof(opts));
+ opts.type = LOWDOWN_HTML;
+ opts.feat = LOWDOWN_AUTOLINK | LOWDOWN_TABLES | LOWDOWN_FENCED |
+ LOWDOWN_STRIKE | LOWDOWN_SUPER | LOWDOWN_COMMONMARK |
+ LOWDOWN_DEFLIST | LOWDOWN_ATTRS;
+ /*
+ * OWASP-sanitize any raw HTML embedded in the README: this content
+ * comes from repository data which gotwebd must not trust blindly.
+ */
+ opts.oflags = LOWDOWN_HTML_HEAD_IDS | LOWDOWN_HTML_NUM_ENT |
+ LOWDOWN_HTML_OWASP | LOWDOWN_SMARTY;
+
+ if (!lowdown_buf(&opts, (const char *)buf, len, &out, &outlen, NULL))
+ return -1;
+
+ /* lowdown_buf() does not guarantee NUL-termination. */
+ *html = malloc(outlen + 1);
+ if (*html == NULL) {
+ free(out);
+ return -1;
+ }
+ memcpy(*html, out, outlen);
+ (*html)[outlen] = '\0';
+ free(out);
+ return 0;
+}
+
+/*
+ * Return non-zero if the given README file name should be rendered as
+ * Markdown (case-insensitive ".md" suffix), matching GitHub's convention.
+ */
+int
+got_readme_is_markdown(const char *name)
+{
+ size_t len = strlen(name);
+
+ if (len < 3)
+ return 0;
+ return strcasecmp(name + len - 3, ".md") == 0;
+}
+
const struct got_error *
got_open_blob_for_output(struct got_blob_object **blob, int *fd,
int *binary, struct request *c, const char *directory, const char *file,
diff --git a/gotwebd/gotwebd.h b/gotwebd/gotwebd.h
index 09387b7..adcf4ba 100644
--- a/gotwebd/gotwebd.h
+++ b/gotwebd/gotwebd.h
@@ -703,6 +703,8 @@ const struct got_error *got_get_repo_heads(struct request *);
const struct got_error *got_open_diff_for_output(FILE **, struct request *);
int got_output_repo_tree(struct request *, char **,
int (*)(struct template *, struct got_tree_entry *));
+int got_render_readme_markdown(char **, const uint8_t *, size_t);
+int got_readme_is_markdown(const char *);
const struct got_error *got_open_blob_for_output(struct got_blob_object **,
int *, int *, struct request *, const char *, const char *, const char *);
int got_output_blob_by_lines(struct template *, struct got_blob_object *,
diff --git a/gotwebd/pages.tmpl b/gotwebd/pages.tmpl
index a148101..26560ac 100644
--- a/gotwebd/pages.tmpl
+++ b/gotwebd/pages.tmpl
@@ -737,7 +737,11 @@ nextsep(char *s, char **t)
const struct querystring *qs = c->t->qs;
struct gotweb_url url;
char *readme = NULL;
+ char *readme_buf = NULL;
+ char *readme_html = NULL;
+ size_t readme_bufsz = 0;
int binary;
+ int is_markdown = 0;
const uint8_t *buf;
size_t len;
!}
@@ -746,6 +750,8 @@ nextsep(char *s, char **t)
</table>
{{ if readme }}
{!
+ is_markdown = got_readme_is_markdown(readme);
+
error = got_open_blob_for_output(&t->blob, &t->fd, &binary, c,
qs->folder[0] ? qs->folder : NULL, readme,
qs->commit[0] ? qs->commit : NULL);
@@ -768,6 +774,47 @@ nextsep(char *s, char **t)
{{ readme }}
</a>
</h2>
+ {{ if is_markdown }}
+ {!
+ /* Slurp the blob so lowdown can parse it as one buffer. */
+ for (;;) {
+ char *tmp;
+
+ error = got_object_blob_read_block(&len, t->blob);
+ if (error) {
+ free(readme);
+ free(readme_buf);
+ return (-1);
+ }
+ if (len == 0)
+ break;
+ buf = got_object_blob_get_read_buf(t->blob);
+ tmp = realloc(readme_buf, readme_bufsz + len);
+ if (tmp == NULL) {
+ free(readme);
+ free(readme_buf);
+ return (-1);
+ }
+ readme_buf = tmp;
+ memcpy(readme_buf + readme_bufsz, buf, len);
+ readme_bufsz += len;
+ }
+ if (got_render_readme_markdown(&readme_html,
+ (uint8_t *)readme_buf, readme_bufsz) == -1) {
+ free(readme);
+ free(readme_buf);
+ return (-1);
+ }
+ !}
+ <div class="readme markdown-body">
+ {! if (tp_write(tp, readme_html, strlen(readme_html)) == -1) {
+ free(readme);
+ free(readme_buf);
+ free(readme_html);
+ return (-1);
+ } !}
+ </div>
+ {{ else }}
<pre>
{!
for (;;) {
@@ -786,10 +833,15 @@ nextsep(char *s, char **t)
}
!}
</pre>
+ {{ end }}
{{ end }}
{{ end }}
{{ finally }}
- {! free(readme); !}
+ {!
+ free(readme);
+ free(readme_buf);
+ free(readme_html);
+ !}
{{ end }}
{{ define gotweb_render_tree(struct template *tp) }}
Index: gotwebd/Makefile
===================================================================
--- gotwebd/Makefile
+++ gotwebd/Makefile
@@ -30,7 +30,7 @@
MAN = ${PROG}.conf.5 ${PROG}.8
CPPFLAGS += -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}
CPPFLAGS += -I${.CURDIR}/../template
-LDADD += -lz -levent -lutil -lm -lcrypto
+LDADD += -lz -levent -lutil -lm -lcrypto -llowdown
YFLAGS =
DPADD = ${LIBEVENT} ${LIBUTIL} ${LIBM} ${LIBCRYPTO}
#CFLAGS += -DGOT_NO_OBJ_CACHE
gotwebd: render README.md as HTML, like GitHub does