changeset 73:6792975da9a0

pasterd: start removing themes
author David Demelier <markand@malikania.fr>
date Tue, 21 Feb 2023 22:22:02 +0100
parents 1a98bc0daa49
children 67b3d13a5035
files .hgignore INSTALL.md Makefile extern/LICENSE.bcc.txt extern/VERSION.bcc.txt extern/bcc/bcc.c fmt-paste.c fmt-paste.h fmt.c fmt.h html/footer.html html/header.html html/index.html html/paste-table.html html/paste.html page-index.c page.c page.h util.c util.h
diffstat 20 files changed, 695 insertions(+), 124 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Feb 01 13:12:59 2023 +0100
+++ b/.hgignore	Tue Feb 21 22:22:02 2023 +0100
@@ -1,18 +1,21 @@
 # vim/emacs specific.
+\.swo$
+\.swp$
+^cscope\.out$
 ^tags$
 ^tags.lock$
 ^tags.temp$
-\.swp$
-\.swo$
 
 # macOS specific.
 \.DS_Store$
 
 # Temporary files.
+^extern/bcc/bcc$
 ^libpaster\.a$
 ^libsqlite3\.a$
 \.o$
 \.d$
+^html/.*\.h$
 
 # Manual pages.
 ^paster(\.1)?$
--- a/INSTALL.md	Wed Feb 01 13:12:59 2023 +0100
+++ b/INSTALL.md	Tue Feb 21 22:22:02 2023 +0100
@@ -15,7 +15,7 @@
 
 Quick install.
 
-	$ tar xvzf paster-x.y.z-tar.xz
+	$ tar -xf paster-x.y.z-tar.xz
 	$ cd paster-x.y.z
 	$ make
 	# sudo make install
--- a/Makefile	Wed Feb 01 13:12:59 2023 +0100
+++ b/Makefile	Tue Feb 21 22:22:02 2023 +0100
@@ -19,26 +19,27 @@
 .POSIX:
 
 # User options.
-CC=             cc
 CFLAGS=         -DNDEBUG -O3
 
 # Installation paths.
 PREFIX=         /usr/local
-BINDIR=         ${PREFIX}/bin
-SHAREDIR=       ${PREFIX}/share
-MANDIR=         ${PREFIX}/share/man
-VARDIR=         ${PREFIX}/var
+BINDIR=         $(PREFIX)/bin
+SHAREDIR=       $(PREFIX)/share
+MANDIR=         $(PREFIX)/share/man
+VARDIR=         $(PREFIX)/var
 
-VERSION=        0.2.1
+VERSION=        0.3.0
 
 CORE_SRCS=      config.c                        \
                 database.c                      \
-                http.c                          \
-                log.c                           \
+                fmt.c                           \
+                fmt-paste.c                     \
                 fragment-duration.c             \
                 fragment-language.c             \
                 fragment-paste.c                \
                 fragment.c                      \
+                http.c                          \
+                log.c                           \
                 page-download.c                 \
                 page-fork.c                     \
                 page-index.c                    \
@@ -49,12 +50,19 @@
                 page.c                          \
                 paste.c                         \
                 util.c
-CORE_OBJS=      ${CORE_SRCS:.c=.o}
+CORE_OBJS=      $(CORE_SRCS:.c=.o)
 CORE_LIB=       libpaster.a
 
+HTML_SRCS=      html/footer.html                \
+                html/header.html                \
+                html/index.html                 \
+                html/paste-table.html           \
+                html/paste.html
+HTML_OBJS=      $(HTML_SRCS:.html=.h)
+
 TESTS_SRCS=     tests/test-database.c
-TESTS_OBJS=     ${TESTS_SRCS:.c=.o}
-TESTS=          ${TESTS_SRCS:.c=}
+TESTS_OBJS=     $(TESTS_SRCS:.c=.o)
+TESTS=          $(TESTS_SRCS:.c=)
 
 SQLITE_FLAGS=   -DSQLITE_THREADSAFE=0           \
                 -DSQLITE_OMIT_LOAD_EXTENSION    \
@@ -65,69 +73,77 @@
 KCGI_INCS=      `pkg-config --cflags kcgi kcgi-html`
 KCGI_LIBS=      `pkg-config --libs kcgi kcgi-html`
 
-INCS=           -I. -Iextern ${KCGI_INCS}
-DEFS=           -D_POSIX_C_SOURCE=200809L -DVARDIR=\"${VARDIR}\" -DSHAREDIR=\"${SHAREDIR}\"
-LIBS=           ${KCGI_LIBS}
-SED=            sed -e "s|@SHAREDIR@|${SHAREDIR}|" \
-                    -e "s|@VARDIR@|${VARDIR}|"
+INCS=           -I. -Iextern $(KCGI_INCS)
+DEFS=           -D_POSIX_C_SOURCE=200809L -DVARDIR=\"$(VARDIR)\" -DSHAREDIR=\"$(SHAREDIR)\"
+LIBS=           $(KCGI_LIBS)
+SED=            sed -e "s|@SHAREDIR@|$(SHAREDIR)|" \
+                    -e "s|@VARDIR@|$(VARDIR)|"
+BCC=            extern/bcc/bcc
 
 .SUFFIXES:
-.SUFFIXES: .o .c .sh
+.SUFFIXES: .o .c .h .sh .html
 
 all: pasterd pasterd-clean paster
 
 .c.o:
-	${CC} ${INCS} ${DEFS} ${CFLAGS} -c $< -o $@
+	$(CC) $(INCS) $(DEFS) $(CFLAGS) -c $< -o $@
 
 .o:
-	${CC} ${INCS} ${DEFS} ${CFLAGS} $< -o $@ ${CORE_LIB} ${SQLITE_LIB} ${LIBS} ${LDFLAGS}
+	$(CC) $(INCS) $(DEFS) $(CFLAGS) $< -o $@ $(CORE_LIB) $(SQLITE_LIB) $(LIBS) $(LDFLAGS)
+
+.html.h:
+	$(BCC) -cs0 $< html_${<F} > $@
 
 .sh:
-	${SED} < $< > $@
+	$(SED) < $< > $@
+
+$(SQLITE_LIB): extern/sqlite3.c extern/sqlite3.h
+	$(CC) $(CFLAGS) $(SQLITE_FLAGS) -c extern/sqlite3.c -o extern/sqlite3.o
+	$(AR) -rc $@ extern/sqlite3.o
 
-${SQLITE_LIB}: extern/sqlite3.c extern/sqlite3.h
-	${CC} ${CFLAGS} ${SQLITE_FLAGS} -c extern/sqlite3.c -o extern/sqlite3.o
-	${AR} -rc $@ extern/sqlite3.o
+$(HTML_OBJS): $(BCC)
 
-${CORE_LIB}: ${CORE_OBJS}
-	${AR} -rc $@ ${CORE_OBJS}
+$(CORE_OBJS): $(HTML_OBJS)
+
+$(CORE_LIB): $(CORE_OBJS)
+	$(AR) -rc $@ $(CORE_OBJS)
 
 paster: paster.sh
 	cp paster.sh paster
 	chmod +x paster
 
-pasterd.o pasterd-clean.o: ${CORE_LIB} ${SQLITE_LIB}
+pasterd: $(CORE_LIB) $(SQLITE_LIB)
 
 clean:
-	rm -f ${SQLITE_LIB} extern/sqlite3.o
-	rm -f ${CORE_LIB} ${CORE_OBJS}
+	rm -f $(SQLITE_LIB) extern/sqlite3.o
+	rm -f $(CORE_LIB) $(CORE_OBJS)
 	rm -f paster pasterd-clean pasterd
-	rm -f test.db ${TESTS_OBJS}
+	rm -f test.db $(TESTS_OBJS)
 
 install-paster:
-	mkdir -p ${DESTDIR}${BINDIR}
-	mkdir -p ${DESTDIR}${MANDIR}/man1
-	cp paster ${DESTDIR}${BINDIR}
-	chmod 755 ${DESTDIR}${BINDIR}/paster
-	${SED} < paster.1 > ${DESTDIR}${MANDIR}/man1/paster.1
+	mkdir -p $(DESTDIR)$(BINDIR)
+	mkdir -p $(DESTDIR)$(MANDIR)/man1
+	cp paster $(DESTDIR)$(BINDIR)
+	chmod 755 $(DESTDIR)$(BINDIR)/paster
+	$(SED) < paster.1 > $(DESTDIR)$(MANDIR)/man1/paster.1
 
 install-pasterd:
-	mkdir -p ${DESTDIR}${BINDIR}
-	mkdir -p ${DESTDIR}${MANDIR}/man5
-	mkdir -p ${DESTDIR}${MANDIR}/man8
-	cp pasterd ${DESTDIR}${BINDIR}
-	cp pasterd-clean ${DESTDIR}${BINDIR}
-	mkdir -p ${DESTDIR}${SHAREDIR}/paster
-	cp -R themes ${DESTDIR}${SHAREDIR}/paster
-	${SED} < pasterd.8 > ${DESTDIR}${MANDIR}/man8/pasterd.8
-	${SED} < pasterd-clean.8 > ${DESTDIR}${MANDIR}/man8/pasterd-clean.8
-	${SED} < pasterd-themes.5 > ${DESTDIR}${MANDIR}/man5/pasterd-themes.5
+	mkdir -p $(DESTDIR)$(BINDIR)
+	mkdir -p $(DESTDIR)$(MANDIR)/man5
+	mkdir -p $(DESTDIR)$(MANDIR)/man8
+	cp pasterd $(DESTDIR)$(BINDIR)
+	cp pasterd-clean $(DESTDIR)$(BINDIR)
+	mkdir -p $(DESTDIR)$(SHAREDIR)/paster
+	cp -R themes $(DESTDIR)$(SHAREDIR)/paster
+	$(SED) < pasterd.8 > $(DESTDIR)$(MANDIR)/man8/pasterd.8
+	$(SED) < pasterd-clean.8 > $(DESTDIR)$(MANDIR)/man8/pasterd-clean.8
+	$(SED) < pasterd-themes.5 > $(DESTDIR)$(MANDIR)/man5/pasterd-themes.5
 
 install: install-pasterd install-paster
 
-${TESTS_OBJS}: ${CORE_LIB} ${SQLITE_LIB}
+$(TESTS_OBJS): $(CORE_LIB) $(SQLITE_LIB)
 
-tests: ${TESTS}
-	for t in ${TESTS}; do $$t; done
+tests: $(TESTS)
+	for t in $(TESTS); do $$t; done
 
 .PHONY: all clean tests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/LICENSE.bcc.txt	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,16 @@
+bcc ISC LICENSE
+===============
+
+Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/VERSION.bcc.txt	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,1 @@
+2.0.1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/bcc/bcc.c	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,164 @@
+/*
+ * bcc.c -- binary to C/C++ arrays converter
+ *
+ * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <unistd.h>
+
+static const char *charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
+static char findentchar = '\t';
+static int findent = 1, fconst, fnull, fstatic;
+
+noreturn static void
+usage(void)
+{
+	fprintf(stderr, "usage: bcc [-0cs] [-I tab-indent] [-i space-indent] input variable\n");
+	exit(1);
+}
+
+noreturn static void
+die(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	fputs("abort: ", stderr);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	exit(1);
+}
+
+static char *
+mangle(char *variable)
+{
+	char *p;
+	size_t pos;
+
+	/* Remove extension. */
+	if ((p = strrchr(variable, '.')))
+		*p = '\0';
+
+	/* Remove disallowed characters. */
+	while ((pos = strspn(variable, charset)) != strlen(variable))
+		variable[pos] = '_';
+
+	return variable;
+}
+
+static void
+indent(void)
+{
+	for (int i = 0; i < findent; ++i)
+		putchar(findentchar);
+}
+
+static void
+put(int ch)
+{
+	printf("0x%02hhx", (unsigned char)ch);
+}
+
+static void
+process(const char *input, const char *variable)
+{
+	FILE *fp;
+	int ch, col = 0;
+
+	if (strcmp(input, "-") == 0)
+		fp = stdin;
+	else if (!(fp = fopen(input, "rb")))
+		die("%s: %s\n", input, strerror(errno));
+
+	if (fstatic)
+		printf("static ");
+	if (fconst)
+		printf("const ");
+
+	printf("unsigned char %s[] = {\n", variable);
+
+	for (ch = fgetc(fp); ch != EOF; ) {
+		if (col == 0)
+			indent();
+
+		put(ch);
+
+		if ((ch = fgetc(fp)) != EOF || fnull)
+			printf(",%s", col < 3 ? " " : "");
+
+		if (++col == 4) {
+			col = 0;
+			putchar('\n');
+		}
+
+		/* Add final '\0' if required. */
+		if (ch == EOF && fnull) {
+			if (col++ == 0)
+				indent();
+
+			put(0);
+		}
+	}
+
+	if (col != 0)
+		printf("\n");
+
+	puts("};");
+	fclose(fp);
+}
+
+int
+main(int argc, char **argv)
+{
+	int ch;
+
+	while ((ch = getopt(argc, argv, "0cI:i:s")) != -1) {
+		switch (ch) {
+		case '0':
+			fnull = 1;
+			break;
+		case 'c':
+			fconst = 1;
+			break;
+		case 'I':
+			findentchar = '\t';
+			findent = atoi(optarg);
+			break;
+		case 'i':
+			findentchar = ' ';
+			findent = atoi(optarg);
+			break;
+		case 's':
+			fstatic = 1;
+			break;
+		default:
+			break;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 2)
+		usage();
+
+	process(argv[0], mangle(argv[1]));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fmt-paste.c	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,140 @@
+/*
+ * fmt-paste.c -- page formatter for pastes
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdint.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+
+#include "fmt-paste.h"
+#include "fmt.h"
+#include "paste.h"
+#include "util.h"
+
+#include "html/paste.h"
+#include "html/paste-table.h"
+
+static void
+print_id(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)html;
+
+	khttp_puts(req, ((const struct paste *)data)->id);
+}
+
+static void
+print_title(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)req;
+
+	khtml_puts(html, ((const struct paste *)data)->title);
+}
+
+static void
+print_author(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)req;
+
+	khtml_puts(html, ((const struct paste *)data)->author);
+}
+
+static void
+print_language(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)html;
+
+	khttp_puts(req, ((const struct paste *)data)->language);
+}
+
+static void
+print_date(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)html;
+
+	const struct paste *paste = data;
+
+	khttp_puts(req, bstrftime("%c", localtime(&paste->timestamp)));
+}
+
+static void
+print_expiration(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)html;
+
+	const struct paste *paste = data;
+
+	khttp_puts(req, ttl(paste->timestamp, paste->duration));
+}
+
+#include <stdio.h>
+static void
+print_pastes(struct kreq *req, struct khtmlreq *html, const void *data)
+{
+	(void)html;
+
+	fmt_pastes(req, data);
+}
+
+/*
+ * Generate each column for the given paste.
+ */
+void
+fmt_paste(struct kreq *req, const struct paste *paste)
+{
+	assert(req);
+	assert(paste);
+
+	fmt(req, html_paste, paste, (const struct fmt_printer []) {
+		{ "id",         print_id                },
+		{ "title",      print_title             },
+		{ "author",     print_author            },
+		{ "language",   print_language          },
+		{ "date",       print_date              },
+		{ "expiration", print_expiration        },
+		{ NULL,         NULL                    }
+	});
+}
+
+/*
+ * Generate each row from all pastes
+ */
+void
+fmt_pastes(struct kreq *req, const struct fmt_paste_vec *vec)
+{
+	for (size_t i = 0; i < vec->pastesz; ++i)
+		fmt_paste(req, &vec->pastes[i]);
+}
+
+/*
+ * Generate an HTML table with all pastes inside as long as there is a
+ * keyword.
+ */
+void
+fmt_paste_table(struct kreq *req, const struct fmt_paste_vec *vec)
+{
+	assert(req);
+	assert(pastes);
+
+	fmt(req, html_paste_table, vec, (const struct fmt_printer []) {
+		{ "pastes",     print_pastes    },
+		{ NULL,         NULL            }
+	});
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fmt-paste.h	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,41 @@
+/*
+ * fmt-paste.h -- page formatter for pastes
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PASTER_FMT_PASTE_H
+#define PASTER_FMT_PASTE_H
+
+#include <stddef.h>
+
+struct kreq;
+struct paste;
+
+struct fmt_paste_vec {
+	const struct paste *pastes;
+	size_t pastesz;
+};
+
+void
+fmt_paste(struct kreq *, const struct paste *);
+
+void
+fmt_pastes(struct kreq *, const struct fmt_paste_vec *);
+
+void
+fmt_paste_table(struct kreq *, const struct fmt_paste_vec *);
+
+#endif /* !PASTER_FMT_PASTE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fmt.c	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,108 @@
+/*
+ * fmt.c -- page formatter
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <err.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+
+#include "fmt.h"
+#include "util.h"
+
+struct tpl {
+	struct kreq *req;
+	struct khtmlreq html;
+	const struct fmt_printer *printers;
+	const void *data;
+};
+
+static int
+callback(size_t index, void *arg)
+{
+	struct tpl *tpl = arg;
+
+	tpl->printers[index].printer(tpl->req, &tpl->html, tpl->data);
+
+	return 1;
+}
+
+static void
+kt_init(struct ktemplate *kt, struct tpl *tpl)
+{
+	/*
+	 * We need to create a list of strings to be set in kt->key from the
+	 * printer list.
+	 */
+	size_t count = 0;
+	const char **list;
+
+	while (tpl->printers[count].keyword)
+		count++;
+
+	list = ecalloc(count, sizeof (char *));
+
+	for (size_t i = 0; i < count; ++i)
+		list[i] = tpl->printers[i].keyword;
+
+	kt->key = list;
+	kt->keysz = count;
+	kt->arg = tpl;
+	kt->cb = callback;
+
+	/* HTML printer. */
+	if (khtml_open(&tpl->html, tpl->req, 0) != KCGI_OK)
+		errx(1, "khtml_open");
+}
+
+static inline void
+kt_free(struct ktemplate *kt, struct tpl *tpl)
+{
+	free((const char **)kt->key);
+	khtml_close(&tpl->html);
+}
+
+void
+fmt(struct kreq *req,
+    const unsigned char *html,
+    const void *data,
+    const struct fmt_printer *printers)
+{
+	assert(req);
+	assert(html);
+
+	const char *str = (const char *)html;
+	struct ktemplate kt;
+	struct tpl tpl = {
+		.req = req,
+		.printers = printers,
+		.data = data
+	};
+
+	if (printers) {
+		kt_init(&kt, &tpl);
+		khttp_template_buf(req, &kt, str, strlen(str));
+		kt_free(&kt, &tpl);
+	} else
+		khttp_template_buf(req, NULL, str, strlen(str));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fmt.h	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,34 @@
+/*
+ * fmt.h -- page formatter
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PASTER_FMT_H
+#define PASTER_FMT_H
+
+struct kreq;
+struct khtmlreq;
+struct paste;
+
+struct fmt_printer {
+	const char *keyword;
+	void (*printer)(struct kreq *, struct khtmlreq *, const void *);
+};
+
+void
+fmt(struct kreq *, const unsigned char *, const void *, const struct fmt_printer *);
+
+#endif /* !PASTER_FMT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/footer.html	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,2 @@
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/header.html	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>@@title@@</title>
+	</head>
+
+	<body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/index.html	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,3 @@
+		<h1>Recent pastes</h1>
+
+@@paste-table@@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/paste-table.html	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,14 @@
+		<table>
+			<thead>
+				<tr>
+					<th>Name</th>
+					<th>Author</th>
+					<th>Language</th>
+					<th>Date</th>
+					<th>Expires in</th>
+				<tr>
+			</thead>
+			<tbody>
+@@pastes@@
+			</tbody>
+		</table>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/paste.html	Tue Feb 21 22:22:02 2023 +0100
@@ -0,0 +1,7 @@
+				<tr>
+					<td><a href="/paste/@@id@@">@@title@@</a></td>
+					<td>@@author@@</td>
+					<td>@@language@@</td>
+					<td>@@date@@</td>
+					<td>@@expiration@@</td>
+				</tr>
--- a/page-index.c	Wed Feb 01 13:12:59 2023 +0100
+++ b/page-index.c	Tue Feb 21 22:22:02 2023 +0100
@@ -16,45 +16,25 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/types.h>
 #include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-
-#include <kcgi.h>
 
 #include "database.h"
+#include "fmt-paste.h"
+#include "fmt.h"
 #include "fragment-paste.h"
 #include "page-index.h"
 #include "page.h"
 #include "paste.h"
 #include "util.h"
 
-struct template {
-	struct kreq *req;
-	const struct paste *pastes;
-	size_t pastesz;
-};
+#include "html/index.h"
 
-static const char *keywords[] = {
-	"pastes"
-};
-
-static int
-template(size_t keyword, void *arg)
+static void
+print_paste_table(struct kreq *req, struct khtmlreq *html, const void *data)
 {
-	struct template *tp = arg;
+	(void)html;
 
-	switch (keyword) {
-	case 0:
-		for (size_t i = 0; i < tp->pastesz; ++i)
-			fragment_paste(tp->req, &tp->pastes[i]);
-		break;
-	default:
-		break;
-	}
-
-	return 1;
+	fmt_paste_table(req, data);
 }
 
 static void
@@ -65,30 +45,29 @@
 
 	if (!database_recents(pastes, &pastesz))
 		page(r, NULL, KHTTP_500, "pages/500.html", "500");
-	else
+	else {
 		page_index_render(r, pastes, pastesz);
 
-	for (size_t i = 0; i < pastesz; ++i)
-		paste_finish(&pastes[i]);
+		for (size_t i = 0; i < pastesz; ++i)
+			paste_finish(&pastes[i]);
+	}
 }
 
 void
 page_index_render(struct kreq *r, const struct paste *pastes, size_t pastesz)
 {
-	struct template data = {
-		.req = r,
+	assert(r);
+	assert(pastes);
+
+	struct fmt_paste_vec vec = {
 		.pastes = pastes,
 		.pastesz = pastesz
 	};
 
-	struct ktemplate kt = {
-		.key = keywords,
-		.keysz = NELEM(keywords),
-		.arg = &data,
-		.cb = template
-	};
-
-	page(r, &kt, KHTTP_200, "pages/index.html", "Recent pastes");
+	page2(r, KHTTP_200, "recent pastes", html_index, &vec, (const struct fmt_printer []) {
+		{ "paste-table",        print_paste_table       },
+		{ NULL,                 NULL                    }
+	});
 }
 
 void
--- a/page.c	Wed Feb 01 13:12:59 2023 +0100
+++ b/page.c	Tue Feb 21 22:22:02 2023 +0100
@@ -16,53 +16,62 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/types.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+
+#include "fmt.h"
 #include "page.h"
 #include "util.h"
 
-struct template {
-	struct kreq *req;
-	const char *title;
-};
+#include "html/header.h"
+#include "html/footer.h"
 
-static const char * const keywords[] = {
-	"title"
-};
-
-static int
-template(size_t keyword, void *arg)
+static void
+print_title(struct kreq *req, struct khtmlreq *html, const void *data)
 {
-	struct template *tp = arg;
+	(void)req;
 
-	switch (keyword) {
-	case 0:
-		khttp_printf(tp->req, "%s", tp->title);
-		break;
-	default:
-		break;
-	}
-
-	return 1;
+	khtml_printf(html, "%s", (const char *)data);
 }
 
 void
 page(struct kreq *req, const struct ktemplate *tmpl, enum khttp status, const char *file, const char *title)
 {
-	struct template data = {
-		.req = req,
-		.title = title
-	};
-	struct ktemplate kt = {
-		.key = keywords,
-		.keysz = NELEM(keywords),
-		.arg = &data,
-		.cb = template
-	};
-
 	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML]);
 	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
 	khttp_body(req);
-	khttp_template(req, &kt, path("fragments/header.html"));
-	khttp_template(req, tmpl, path(file));
-	khttp_template(req, NULL, path("fragments/footer.html"));
+
+	fmt(req, html_header, title, (const struct fmt_printer []) {
+		{ "title",      print_title     },
+		{ NULL,         NULL            }
+	});
+	fmt(req, html_footer, NULL, NULL);
 	khttp_free(req);
 }
+
+void
+page2(struct kreq *req,
+      enum khttp status,
+      const char *title,
+      const unsigned char *html,
+      const void *data,
+      const struct fmt_printer *printers)
+{
+	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML]);
+	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
+	khttp_body(req);
+
+	fmt(req, html_header, title, (const struct fmt_printer []) {
+		{ "title",      print_title     },
+		{ NULL,         NULL            }
+	});
+	fmt(req, html, data, printers);
+	fmt(req, html_footer, NULL, NULL);
+	khttp_free(req);
+}
--- a/page.h	Wed Feb 01 13:12:59 2023 +0100
+++ b/page.h	Tue Feb 21 22:22:02 2023 +0100
@@ -24,7 +24,17 @@
 #include <stdint.h>
 #include <kcgi.h>
 
+struct fmt_printer;
+
 void
 page(struct kreq *, const struct ktemplate *, enum khttp, const char *, const char *);
 
+void
+page2(struct kreq *,
+      enum khttp,
+      const char *,
+      const unsigned char *,
+      const void *,
+      const struct fmt_printer *);
+
 #endif /* !PASTER_PAGE_H */
--- a/util.c	Wed Feb 01 13:12:59 2023 +0100
+++ b/util.c	Tue Feb 21 22:22:02 2023 +0100
@@ -238,6 +238,17 @@
 	return strcpy(ret, str);
 }
 
+void *
+ecalloc(size_t n, size_t w)
+{
+	void *ptr;
+
+	if (!(ptr = calloc(n, w)))
+		die(strerror(errno));
+
+	return ptr;
+}
+
 const char *
 bprintf(const char *fmt, ...)
 {
--- a/util.h	Wed Feb 01 13:12:59 2023 +0100
+++ b/util.h	Tue Feb 21 22:22:02 2023 +0100
@@ -25,6 +25,7 @@
 #define NELEM(x) (sizeof (x) / sizeof (x)[0])
 
 struct tm;
+struct kreq;
 
 extern const char *languages[];
 extern const size_t languagesz;
@@ -35,6 +36,9 @@
 char *
 estrdup(const char *);
 
+void *
+ecalloc(size_t, size_t);
+
 const char *
 bprintf(const char *, ...);