changeset 79:52029a52a385

pasterd: revert using ktemplate
author David Demelier <markand@malikania.fr>
date Fri, 17 Mar 2023 07:43:20 +0100
parents 9bfe5ce3cc45
children 9bc744a4a292
files GNUmakefile database.c database.h extern/LICENSE.libmustach.txt extern/VERSION.libmustach.txt extern/libmustach/mustach-jansson.c extern/libmustach/mustach-jansson.h extern/libmustach/mustach-wrap.c extern/libmustach/mustach-wrap.h extern/libmustach/mustach.c extern/libmustach/mustach.h html/header.html html/index.html html/new.html html/paste.html html/search.html html/status.html http.c json-util.c json-util.h log.c page-download.c page-fork.c page-index.c page-index.h page-new.c page-new.h page-paste.c page-paste.h page-search.c page-status.c page-status.h page.c page.h paste.c paste.h pasterd.c util.c util.h
diffstat 39 files changed, 771 insertions(+), 2511 deletions(-) [+]
line wrap: on
line diff
--- a/GNUmakefile	Thu Mar 16 20:45:59 2023 +0100
+++ b/GNUmakefile	Fri Mar 17 07:43:20 2023 +0100
@@ -30,25 +30,17 @@
 VARDIR ?=       $(PREFIX)/var
 
 # External libraries
-KCGI_INCS :=    $(shell pkg-config --cflags kcgi)
-KCGI_LIBS :=    $(shell pkg-config --libs kcgi)
-
-JANSSON_INCS := $(shell pkg-config --cflags jansson)
-JANSSON_LIBS := $(shell pkg-config --libs jansson)
+KCGI_INCS :=    $(shell pkg-config --cflags kcgi kcgi-html)
+KCGI_LIBS :=    $(shell pkg-config --libs kcgi kcgi-html)
 
 # No user options below this line.
 
 VERSION :=              0.3.0
 
-LIBPASTER_SRCS :=       extern/libmustach/mustach-jansson.c
-LIBPASTER_SRCS +=       extern/libmustach/mustach-wrap.c
-LIBPASTER_SRCS +=       extern/libmustach/mustach.c
-LIBPASTER_SRCS +=       extern/libmustach/mustach.c
 LIBPASTER_SRCS +=       extern/libsqlite/sqlite3.c
 LIBPASTER_SRCS +=       config.c
 LIBPASTER_SRCS +=       database.c
 LIBPASTER_SRCS +=       http.c
-LIBPASTER_SRCS +=       json-util.c
 LIBPASTER_SRCS +=       log.c
 LIBPASTER_SRCS +=       page-download.c
 LIBPASTER_SRCS +=       page-fork.c
@@ -57,7 +49,9 @@
 LIBPASTER_SRCS +=       page-paste.c
 LIBPASTER_SRCS +=       page-search.c
 LIBPASTER_SRCS +=       page-static.c
+LIBPASTER_SRCS +=       page-status.c
 LIBPASTER_SRCS +=       page.c
+LIBPASTER_SRCS +=       paste.c
 LIBPASTER_SRCS +=       util.c
 LIBPASTER_OBJS :=       $(LIBPASTER_SRCS:.c=.o)
 LIBPASTER_DEPS :=       $(LIBPASTER_SRCS:.c=.d)
@@ -92,10 +86,8 @@
 override CFLAGS +=      -DVARDIR=\"$(VARDIR)\"
 override CFLAGS +=      -I.
 override CFLAGS +=      -Iextern
-override CFLAGS +=      -Iextern/libmustach
 override CFLAGS +=      -Iextern/libsqlite
 override CFLAGS +=      $(KCGI_INCS)
-override CFLAGS +=      $(JANSSON_INCS)
 
 override CPPFLAGS :=    -MMD
 
@@ -123,7 +115,7 @@
 $(LIBPASTER_SRCS): $(LIBPASTER_HTML_OBJS) $(LIBPASTER_SQL_OBJS)
 $(LIBPASTER): $(LIBPASTER_OBJS)
 
-pasterd: private LDLIBS += $(KCGI_LIBS) $(JANSSON_LIBS)
+pasterd: private LDLIBS += $(KCGI_LIBS)
 pasterd: $(LIBPASTER)
 
 clean:
--- a/database.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/database.c	Fri Mar 17 07:43:20 2023 +0100
@@ -24,8 +24,8 @@
 #include <sqlite3.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "log.h"
+#include "paste.h"
 #include "util.h"
 
 #include "sql/clear.h"
@@ -35,23 +35,27 @@
 #include "sql/recents.h"
 #include "sql/search.h"
 
-#define ID_MAX (12 + 1)
+#define CHAR(sql) (const char *)(sql)
 
 static sqlite3 *db;
 
-static inline json_t *
-convert(sqlite3_stmt *stmt)
+static char *
+dup(const unsigned char *s)
+{
+	return estrdup(s ? (const char *)(s) : "");
+}
+
+static void
+convert(sqlite3_stmt *stmt, struct paste *paste)
 {
-	return json_pack("{ss ss ss ss ss sI si si}",
-		"id",           sqlite3_column_text(stmt, 0),
-		"title",        sqlite3_column_text(stmt, 1),
-		"author",       sqlite3_column_text(stmt, 2),
-		"language",     sqlite3_column_text(stmt, 3),
-		"code",         sqlite3_column_text(stmt, 4),
-		"timestamp",    (json_int_t)sqlite3_column_int64(stmt, 5),
-		"visible",      sqlite3_column_int(stmt, 6),
-		"duration",     sqlite3_column_int(stmt, 7)
-	);
+	paste->id = dup(sqlite3_column_text(stmt, 0));
+	paste->title = dup(sqlite3_column_text(stmt, 1));
+	paste->author = dup(sqlite3_column_text(stmt, 2));
+	paste->language = dup(sqlite3_column_text(stmt, 3));
+	paste->code = dup(sqlite3_column_text(stmt, 4));
+	paste->timestamp = sqlite3_column_int64(stmt, 5);
+	paste->visible = sqlite3_column_int(stmt, 6);
+	paste->duration = sqlite3_column_int64(stmt, 7);
 }
 
 static int
@@ -62,7 +66,7 @@
 	sqlite3_stmt *stmt = NULL;
 	int ret = 1;
 
-	if (sqlite3_prepare(db, sql_get, -1, &stmt, NULL) == SQLITE_OK) {
+	if (sqlite3_prepare(db, CHAR(sql_get), -1, &stmt, NULL) == SQLITE_OK) {
 		sqlite3_bind_text(stmt, 1, id, -1, NULL);
 		ret = sqlite3_step(stmt) == SQLITE_ROW;
 		sqlite3_finalize(stmt);
@@ -72,19 +76,24 @@
 }
 
 static const char *
-create_id(char *id)
+create_id(void)
 {
 	static const char table[] = "abcdefghijklmnopqrstuvwxyz1234567890";
+	static char id[12];
 
-	for (int i = 0; i < ID_MAX; ++i)
+	for (size_t i = 0; i < sizeof (id); ++i)
 		id[i] = table[rand() % (sizeof (table) - 1)];
 
-	id[ID_MAX - 1] = 0;
+	return id;
 }
 
 static int
-set_id(json_t *paste)
+set_id(struct paste *paste)
 {
+	assert(paste);
+
+	paste->id = NULL;
+
 	/*
 	 * Avoid infinite loop, we only try to create a new id in 30 steps.
 	 *
@@ -92,18 +101,13 @@
 	 * not try to save with that id.
 	 */
 	int tries = 0;
-	char id[ID_MAX];
 
 	do {
-		create_id(id);
-	} while (++tries < 30 && exists(id));
+		free(paste->id);
+		paste->id = estrdup(create_id());
+	} while (++tries < 30 && exists(paste->id));
 
-	if (tries >= 30)
-		return -1;
-
-	json_object_set_new(paste, "id", json_string(id));
-
-	return 0;
+	return tries < 30 ? 0 : -1;
 }
 
 int
@@ -121,7 +125,7 @@
 	/* Wait for 30 seconds to lock the database. */
 	sqlite3_busy_timeout(db, 30000);
 
-	if (sqlite3_exec(db, sql_init, NULL, NULL, NULL) != SQLITE_OK) {
+	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK) {
 		log_warn("database: unable to initialize %s: %s", path, sqlite3_errmsg(db));
 		return -1;
 	}
@@ -129,28 +133,30 @@
 	return 0;
 }
 
-json_t *
-database_recents(size_t limit)
+int
+database_recents(struct paste *pastes, size_t *max)
 {
-	json_t *array = NULL;
+	assert(pastes);
+	assert(max);
+
 	sqlite3_stmt *stmt = NULL;
 	size_t i = 0;
 
+	memset(pastes, 0, *max * sizeof (struct paste));
 	log_debug("database: accessing most recents");
 
-	if (sqlite3_prepare(db, sql_recents, -1, &stmt, NULL) != SQLITE_OK ||
-	    sqlite3_bind_int64(stmt, 1, limit) != SQLITE_OK)
+	if (sqlite3_prepare(db, CHAR(sql_recents), -1, &stmt, NULL) != SQLITE_OK ||
+	    sqlite3_bind_int64(stmt, 1, *max) != SQLITE_OK)
 		goto sqlite_err;
 
-	array = json_array();
-
-	for (; i < limit && sqlite3_step(stmt) == SQLITE_ROW; ++i)
-		json_array_append_new(array, convert(stmt));
+	for (; i < *max && sqlite3_step(stmt) == SQLITE_ROW; ++i)
+		convert(stmt, &pastes[i]);
 
 	log_debug("database: found %zu pastes", i);
 	sqlite3_finalize(stmt);
+	*max = i;
 
-	return array;
+	return 0;
 
 sqlite_err:
 	log_warn("database: error (recents): %s\n", sqlite3_errmsg(db));
@@ -158,26 +164,31 @@
 	if (stmt)
 		sqlite3_finalize(stmt);
 
-	return NULL;
+	*max = 0;
+
+	return -1;
 }
 
-json_t *
-database_get(const char *id)
+int
+database_get(struct paste *paste, const char *id)
 {
+	assert(paste);
 	assert(id);
 
-	json_t *object = NULL;
 	sqlite3_stmt* stmt = NULL;
+	int found = -1;
 
-	log_debug("database: accessing paste with uuid: %s", id);
+	memset(paste, 0, sizeof (struct paste));
+	log_debug("database: accessing paste with id: %s", id);
 
-	if (sqlite3_prepare(db, sql_get, -1, &stmt, NULL) != SQLITE_OK ||
+	if (sqlite3_prepare(db, CHAR(sql_get), -1, &stmt, NULL) != SQLITE_OK ||
 	    sqlite3_bind_text(stmt, 1, id, -1, NULL) != SQLITE_OK)
 		goto sqlite_err;
 
 	switch (sqlite3_step(stmt)) {
 	case SQLITE_ROW:
-		object = convert(stmt);
+		convert(stmt, paste);
+		found = 0;
 		break;
 	case SQLITE_MISUSE:
 	case SQLITE_ERROR:
@@ -188,7 +199,7 @@
 
 	sqlite3_finalize(stmt);
 
-	return object;
+	return found;
 
 sqlite_err:
 	if (stmt)
@@ -196,11 +207,11 @@
 
 	log_warn("database: error (get): %s", sqlite3_errmsg(db));
 
-	return NULL;
+	return -1;
 }
 
 int
-database_insert(json_t *paste)
+database_insert(struct paste *paste)
 {
 	assert(paste);
 
@@ -212,23 +223,21 @@
 		log_warn("database: could not lock database: %s", sqlite3_errmsg(db));
 		return -1;
 	}
-
 	if (set_id(paste) < 0) {
 		log_warn("database: unable to randomize unique identifier");
 		sqlite3_exec(db, "END TRANSACTION", NULL, NULL, NULL);
 		return -1;
 	}
-
-	if (sqlite3_prepare(db, sql_insert, -1, &stmt, NULL) != SQLITE_OK)
+	if (sqlite3_prepare(db, CHAR(sql_insert), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite_err;
 
-	sqlite3_bind_text(stmt, 1, ju_get_string(paste, "id"), -1, SQLITE_STATIC);
-	sqlite3_bind_text(stmt, 2, ju_get_string(paste, "title"), -1, SQLITE_STATIC);
-	sqlite3_bind_text(stmt, 3, ju_get_string(paste, "author"), -1, SQLITE_STATIC);
-	sqlite3_bind_text(stmt, 4, ju_get_string(paste, "language"), -1, SQLITE_STATIC);
-	sqlite3_bind_text(stmt, 5, ju_get_string(paste, "code"), -1, SQLITE_STATIC);
-	sqlite3_bind_int(stmt, 6, ju_get_bool(paste, "visible"));
-	sqlite3_bind_int64(stmt, 7, ju_get_int(paste, "duration"));
+	sqlite3_bind_text(stmt, 1, paste->id, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 2, paste->title, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 3, paste->author, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 4, paste->language, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 5, paste->code, -1, SQLITE_STATIC);
+	sqlite3_bind_int(stmt, 6, paste->visible);
+	sqlite3_bind_int64(stmt, 7, paste->duration);
 
 	if (sqlite3_step(stmt) != SQLITE_DONE)
 		goto sqlite_err;
@@ -237,10 +246,7 @@
 	sqlite3_finalize(stmt);
 
 	log_info("database: new paste (%s) from %s expires in one %lld seconds",
-	    ju_get_string(paste, "id"),
-	    ju_get_string(paste, "author"),
-	    ju_get_int(paste, "duration")
-	);
+	    paste->id, paste->author, paste->duration);
 
 	return 0;
 
@@ -251,19 +257,22 @@
 	if (stmt)
 		sqlite3_finalize(stmt);
 
-	/* Make sure it is not used anymore. */
-	json_object_del(paste, "id");
+	free(paste->id);
+	paste->id = NULL;
 
-	return 0;
+	return -1;
 }
 
-json_t *
-database_search(size_t limit,
+int
+database_search(struct paste *pastes,
+                size_t *max,
                 const char *title,
                 const char *author,
                 const char *language)
 {
-	json_t *array = NULL;
+	assert(pastes);
+	assert(max);
+
 	sqlite3_stmt *stmt = NULL;
 	size_t i = 0;
 
@@ -272,12 +281,14 @@
 	    author   ? author   : "",
 	    language ? language : "");
 
+	memset(pastes, 0, *max * sizeof (struct paste));
+
 	/* Select everything if not specified. */
 	title    = title    ? title    : "%";
 	author   = author   ? author   : "%";
 	language = language ? language : "%";
 
-	if (sqlite3_prepare(db, sql_search, -1, &stmt, NULL) != SQLITE_OK)
+	if (sqlite3_prepare(db, CHAR(sql_search), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite_err;
 	if (sqlite3_bind_text(stmt, 1, title, -1, NULL) != SQLITE_OK)
 		goto sqlite_err;
@@ -285,18 +296,17 @@
 		goto sqlite_err;
 	if (sqlite3_bind_text(stmt, 3, language, -1, NULL) != SQLITE_OK)
 		goto sqlite_err;
-	if (sqlite3_bind_int64(stmt, 4, limit) != SQLITE_OK)
+	if (sqlite3_bind_int64(stmt, 4, *max) != SQLITE_OK)
 		goto sqlite_err;
 
-	array = json_array();
-
-	for (; i < limit && sqlite3_step(stmt) == SQLITE_ROW; ++i)
-		json_array_append_new(array, convert(stmt));
+	for (; i < *max && sqlite3_step(stmt) == SQLITE_ROW; ++i)
+		convert(stmt, &pastes[i]);
 
 	log_debug("database: found %zu pastes", i);
 	sqlite3_finalize(stmt);
+	*max = i;
 
-	return array;
+	return 0;
 
 sqlite_err:
 	log_warn("database: error (search): %s\n", sqlite3_errmsg(db));
@@ -304,7 +314,9 @@
 	if (stmt)
 		sqlite3_finalize(stmt);
 
-	return NULL;
+	*max = 0;
+
+	return -1;
 }
 
 void
@@ -312,7 +324,7 @@
 {
 	log_debug("database: clearing deprecated pastes");
 
-	if (sqlite3_exec(db, sql_clear, NULL, NULL, NULL) != SQLITE_OK)
+	if (sqlite3_exec(db, CHAR(sql_clear), NULL, NULL, NULL) != SQLITE_OK)
 		log_warn("database: error (clear): %s\n", sqlite3_errmsg(db));
 }
 
--- a/database.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/database.h	Fri Mar 17 07:43:20 2023 +0100
@@ -21,77 +21,30 @@
 
 #include <stddef.h>
 
-#include <jansson.h>
+struct paste;
 
-/**
- * Open the database specified by path.
- *
- * \pre path != NULL
- * \param path path to the SQLite file
- * \return 0 on success or -1 on error
- */
 int
-database_open(const char *path);
+database_open(const char *);
 
-/**
- * Obtain a list of recent pastes.
- *
- * \param limit max number of items to fetch
- * \return a JSON array of paste objects or NULL on failure
- */
-json_t *
-database_recents(size_t limit);
+int
+database_recents(struct paste *, size_t *);
 
-/**
- * Obtain a specific paste by id.
- *
- * \pre id != NULL
- * \param id the paste identifier
- * \return NULL on failure or if not found
- */
-json_t *
-database_get(const char *id);
+int
+database_get(struct paste *, const char *);
 
-/**
- * Insert a new paste into the database.
- *
- * On insertion, the paste gets a new string "id" property generated from the
- * database.
- *
- * \pre paste != NULL
- * \param paste the paste object
- * \return 0 on success or -1 on failure
- */
 int
-database_insert(json_t *paste);
+database_insert(struct paste *);
 
-/**
- * Search for pastes based on criterias.
- *
- * If any of the criterias is NULL it is considered as ignored (and then match a
- * database item).
- *
- * \param limit max number of items to fetch
- * \param title paste title (or NULL to match any)
- * \param author paste author (or NULL to match any)
- * \param language paste language (or NULL to match any)
- * \return a JSON array of objects or NULL on failure
- */
-json_t *
-database_search(size_t limit,
-                const char *title,
-                const char *author,
-                const char *language);
+int
+database_search(struct paste *,
+                size_t *,
+                const char *,
+                const char *,
+                const char *);
 
-/**
- * Cleanup expired pastes.
- */
 void
 database_clear(void);
 
-/**
- * Close the database handle
- */
 void
 database_finish(void);
 
--- a/extern/LICENSE.libmustach.txt	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-
-Copyright (c) 2017-2020 by José Bollo
-
-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 ISC DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL ISC 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.
--- a/extern/VERSION.libmustach.txt	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-1.2.5
--- a/extern/libmustach/mustach-jansson.c	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,251 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#define _GNU_SOURCE
-
-#include <stdio.h>
-#include <string.h>
-
-#include "mustach.h"
-#include "mustach-wrap.h"
-#include "mustach-jansson.h"
-
-struct expl {
-	json_t *root;
-	json_t *selection;
-	int depth;
-	struct {
-		json_t *cont;
-		json_t *obj;
-		void *iter;
-		int is_objiter;
-		size_t index, count;
-	} stack[MUSTACH_MAX_DEPTH];
-};
-
-static int start(void *closure)
-{
-	struct expl *e = closure;
-	e->depth = 0;
-	e->selection = json_null();
-	e->stack[0].cont = NULL;
-	e->stack[0].obj = e->root;
-	e->stack[0].index = 0;
-	e->stack[0].count = 1;
-	return MUSTACH_OK;
-}
-
-static int compare(void *closure, const char *value)
-{
-	struct expl *e = closure;
-	json_t *o = e->selection;
-	double d;
-	json_int_t i;
-
-	switch (json_typeof(o)) {
-	case JSON_REAL:
-		d = json_number_value(o) - atof(value);
-		return d < 0 ? -1 : d > 0 ? 1 : 0;
-	case JSON_INTEGER:
-		i = (json_int_t)json_integer_value(o) - (json_int_t)atoll(value);
-		return i < 0 ? -1 : i > 0 ? 1 : 0;
-	case JSON_STRING:
-		return strcmp(json_string_value(o), value);
-	case JSON_TRUE:
-		return strcmp("true", value);
-	case JSON_FALSE:
-		return strcmp("false", value);
-	case JSON_NULL:
-		return strcmp("null", value);
-	default:
-		return 1;
-	}
-}
-
-static int sel(void *closure, const char *name)
-{
-	struct expl *e = closure;
-	json_t *o;
-	int i, r;
-
-	if (name == NULL) {
-		o = e->stack[e->depth].obj;
-		r = 1;
-	} else {
-		i = e->depth;
-		while (i >= 0 && !(o = json_object_get(e->stack[i].obj, name)))
-			i--;
-		if (i >= 0)
-			r = 1;
-		else {
-			o = json_null();
-			r = 0;
-		}
-	}
-	e->selection = o;
-	return r;
-}
-
-static int subsel(void *closure, const char *name)
-{
-	struct expl *e = closure;
-	json_t *o;
-	int r;
-
-	o = json_object_get(e->selection, name);
-	r = o != NULL;
-	if (r)
-		e->selection = o;
-	return r;
-}
-
-static int enter(void *closure, int objiter)
-{
-	struct expl *e = closure;
-	json_t *o;
-
-	if (++e->depth >= MUSTACH_MAX_DEPTH)
-		return MUSTACH_ERROR_TOO_DEEP;
-
-	o = e->selection;
-	e->stack[e->depth].is_objiter = 0;
-	if (objiter) {
-		if (!json_is_object(o))
-			goto not_entering;
-		e->stack[e->depth].iter = json_object_iter(o);
-		if (e->stack[e->depth].iter == NULL)
-			goto not_entering;
-		e->stack[e->depth].obj = json_object_iter_value(e->stack[e->depth].iter);
-		e->stack[e->depth].cont = o;
-		e->stack[e->depth].is_objiter = 1;
-	} else if (json_is_array(o)) {
-		e->stack[e->depth].count = json_array_size(o);
-		if (e->stack[e->depth].count == 0)
-			goto not_entering;
-		e->stack[e->depth].cont = o;
-		e->stack[e->depth].obj = json_array_get(o, 0);
-		e->stack[e->depth].index = 0;
-	} else if ((json_is_object(o) && json_object_size(0)) || (!json_is_false(o) && !json_is_null(o))) {
-		e->stack[e->depth].count = 1;
-		e->stack[e->depth].cont = NULL;
-		e->stack[e->depth].obj = o;
-		e->stack[e->depth].index = 0;
-	} else
-		goto not_entering;
-	return 1;
-
-not_entering:
-	e->depth--;
-	return 0;
-}
-
-static int next(void *closure)
-{
-	struct expl *e = closure;
-
-	if (e->depth <= 0)
-		return MUSTACH_ERROR_CLOSING;
-
-	if (e->stack[e->depth].is_objiter) {
-		e->stack[e->depth].iter = json_object_iter_next(e->stack[e->depth].cont, e->stack[e->depth].iter);
-		if (e->stack[e->depth].iter == NULL)
-			return 0;
-		e->stack[e->depth].obj = json_object_iter_value(e->stack[e->depth].iter);
-		return 1;
-	}
-
-	e->stack[e->depth].index++;
-	if (e->stack[e->depth].index >= e->stack[e->depth].count)
-		return 0;
-
-	e->stack[e->depth].obj = json_array_get(e->stack[e->depth].cont, e->stack[e->depth].index);
-	return 1;
-}
-
-static int leave(void *closure)
-{
-	struct expl *e = closure;
-
-	if (e->depth <= 0)
-		return MUSTACH_ERROR_CLOSING;
-
-	e->depth--;
-	return 0;
-}
-
-static int get(void *closure, struct mustach_sbuf *sbuf, int key)
-{
-	struct expl *e = closure;
-	const char *s;
-
-	if (key) {
-		s = e->stack[e->depth].is_objiter
-			? json_object_iter_key(e->stack[e->depth].iter)
-			: "";
-	}
-	else if (json_is_string(e->selection))
-		s = json_string_value(e->selection);
-	else if (json_is_null(e->selection))
-		s = "";
-	else {
-		s = json_dumps(e->selection, JSON_ENCODE_ANY | JSON_COMPACT);
-		if (s == NULL)
-			return MUSTACH_ERROR_SYSTEM;
-		sbuf->freecb = free;
-	}
-	sbuf->value = s;
-	return 1;
-}
-
-const struct mustach_wrap_itf mustach_jansson_wrap_itf = {
-	.start = start,
-	.stop = NULL,
-	.compare = compare,
-	.sel = sel,
-	.subsel = subsel,
-	.enter = enter,
-	.next = next,
-	.leave = leave,
-	.get = get
-};
-
-int mustach_jansson_file(const char *template, size_t length, json_t *root, int flags, FILE *file)
-{
-	struct expl e;
-	e.root = root;
-	return mustach_wrap_file(template, length, &mustach_jansson_wrap_itf, &e, flags, file);
-}
-
-int mustach_jansson_fd(const char *template, size_t length, json_t *root, int flags, int fd)
-{
-	struct expl e;
-	e.root = root;
-	return mustach_wrap_fd(template, length, &mustach_jansson_wrap_itf, &e, flags, fd);
-}
-
-int mustach_jansson_mem(const char *template, size_t length, json_t *root, int flags, char **result, size_t *size)
-{
-	struct expl e;
-	e.root = root;
-	return mustach_wrap_mem(template, length, &mustach_jansson_wrap_itf, &e, flags, result, size);
-}
-
-int mustach_jansson_write(const char *template, size_t length, json_t *root, int flags, mustach_write_cb_t *writecb, void *closure)
-{
-	struct expl e;
-	e.root = root;
-	return mustach_wrap_write(template, length, &mustach_jansson_wrap_itf, &e, flags, writecb, closure);
-}
-
-int mustach_jansson_emit(const char *template, size_t length, json_t *root, int flags, mustach_emit_cb_t *emitcb, void *closure)
-{
-	struct expl e;
-	e.root = root;
-	return mustach_wrap_emit(template, length, &mustach_jansson_wrap_itf, &e, flags, emitcb, closure);
-}
-
--- a/extern/libmustach/mustach-jansson.h	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#ifndef _mustach_jansson_h_included_
-#define _mustach_jansson_h_included_
-
-/*
- * mustach-jansson is intended to make integration of jansson
- * library by providing integrated functions.
- */
-
-#include <jansson.h>
-#include "mustach-wrap.h"
-
-/**
- * Wrap interface used internally by mustach jansson functions.
- * Can be used for overriding behaviour.
- */
-extern const struct mustach_wrap_itf mustach_jansson_wrap_itf;
-
-/**
- * mustach_jansson_file - Renders the mustache 'template' in 'file' for 'root'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @root:     the root json object to render
- * @file:     the file where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_jansson_file(const char *template, size_t length, json_t *root, int flags, FILE *file);
-
-/**
- * mustach_jansson_fd - Renders the mustache 'template' in 'fd' for 'root'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @root:     the root json object to render
- * @fd:       the file descriptor number where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_jansson_fd(const char *template, size_t length, json_t *root, int flags, int fd);
-
-
-/**
- * mustach_jansson_mem - Renders the mustache 'template' in 'result' for 'root'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @root:     the root json object to render
- * @result:   the pointer receiving the result when 0 is returned
- * @size:     the size of the returned result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_jansson_mem(const char *template, size_t length, json_t *root, int flags, char **result, size_t *size);
-
-/**
- * mustach_jansson_write - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @root:     the root json object to render
- * @writecb:  the function that write values
- * @closure:  the closure for the write function
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_jansson_write(const char *template, size_t length, json_t *root, int flags, mustach_write_cb_t *writecb, void *closure);
-
-/**
- * mustach_jansson_emit - Renders the mustache 'template' for 'root' to custom emiter 'emitcb' with 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @root:     the root json object to render
- * @emitcb:   the function that emit values
- * @closure:  the closure for the write function
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_jansson_emit(const char *template, size_t length, json_t *root, int flags, mustach_emit_cb_t *emitcb, void *closure);
-
-#endif
-
--- a/extern/libmustach/mustach-wrap.c	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,456 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#define _GNU_SOURCE
-
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-#ifdef _WIN32
-#include <malloc.h>
-#endif
-
-#include "mustach.h"
-#include "mustach-wrap.h"
-
-#if !defined(INCLUDE_PARTIAL_EXTENSION)
-# define INCLUDE_PARTIAL_EXTENSION ".mustache"
-#endif
-
-/* global hook for partials */
-int (*mustach_wrap_get_partial)(const char *name, struct mustach_sbuf *sbuf) = NULL;
-
-/* internal structure for wrapping */
-struct wrap {
-	/* original interface */
-	const struct mustach_wrap_itf *itf;
-
-	/* original closure */
-	void *closure;
-
-	/* flags */
-	int flags;
-
-	/* emiter callback */
-	mustach_emit_cb_t *emitcb;
-
-	/* write callback */
-	mustach_write_cb_t *writecb;
-};
-
-/* length given by masking with 3 */
-enum comp {
-	C_no = 0,
-	C_eq = 1,
-	C_lt = 5,
-	C_le = 6,
-	C_gt = 9,
-	C_ge = 10
-};
-
-enum sel {
-	S_none = 0,
-	S_ok = 1,
-	S_objiter = 2,
-	S_ok_or_objiter = S_ok | S_objiter
-};
-
-static enum comp getcomp(char *head, int sflags)
-{
-	return (head[0] == '=' && (sflags & Mustach_With_Equal)) ? C_eq
-		: (head[0] == '<' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_le : C_lt)
-		: (head[0] == '>' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_ge : C_gt)
-		: C_no;
-}
-
-static char *keyval(char *head, int sflags, enum comp *comp)
-{
-	char *w, car, escaped;
-	enum comp k;
-
-	k = C_no;
-	w = head;
-	car = *head;
-	escaped = (sflags & Mustach_With_EscFirstCmp) && (getcomp(head, sflags) != C_no);
-	while (car && (escaped || (k = getcomp(head, sflags)) == C_no)) {
-		if (escaped)
-			escaped = 0;
-		else
-			escaped = ((sflags & Mustach_With_JsonPointer) ? car == '~' : car == '\\')
-			    && (getcomp(head + 1, sflags) != C_no);
-		if (!escaped)
-			*w++ = car;
-		head++;
-		car = *head;
-	}
-	*w = 0;
-	*comp = k;
-	return k == C_no ? NULL : &head[k & 3];
-}
-
-static char *getkey(char **head, int sflags)
-{
-	char *result, *iter, *write, car;
-
-	car = *(iter = *head);
-	if (!car)
-		result = NULL;
-	else {
-		result = write = iter;
-		if (sflags & Mustach_With_JsonPointer)
-		{
-			while (car && car != '/') {
-				if (car == '~')
-					switch (iter[1]) {
-					case '1': car = '/'; /*@fallthrough@*/
-					case '0': iter++;
-					}
-				*write++ = car;
-				car = *++iter;
-			}
-			*write = 0;
-			while (car == '/')
-				car = *++iter;
-		}
-		else
-		{
-			while (car && car != '.') {
-				if (car == '\\' && (iter[1] == '.' || iter[1] == '\\'))
-					car = *++iter;
-				*write++ = car;
-				car = *++iter;
-			}
-			*write = 0;
-			while (car == '.')
-				car = *++iter;
-		}
-		*head = iter;
-	}
-	return result;
-}
-
-static enum sel sel(struct wrap *w, const char *name)
-{
-	enum sel result;
-	int i, j, sflags, scmp;
-	char *key, *value;
-	enum comp k;
-
-	/* make a local writeable copy */
-	size_t lenname = 1 + strlen(name);
-	char buffer[lenname];
-	char *copy = buffer;
-	memcpy(copy, name, lenname);
-
-	/* check if matches json pointer selection */
-	sflags = w->flags;
-	if (sflags & Mustach_With_JsonPointer) {
-		if (copy[0] == '/')
-			copy++;
-		else
-			sflags ^= Mustach_With_JsonPointer;
-	}
-
-	/* extract the value, translate the key and get the comparator */
-	if (sflags & (Mustach_With_Equal | Mustach_With_Compare))
-		value = keyval(copy, sflags, &k);
-	else {
-		k = C_no;
-		value = NULL;
-	}
-
-	/* case of . alone if Mustach_With_SingleDot? */
-	if (copy[0] == '.' && copy[1] == 0 /*&& (sflags & Mustach_With_SingleDot)*/)
-		/* yes, select current */
-		result = w->itf->sel(w->closure, NULL) ? S_ok : S_none;
-	else
-	{
-		/* not the single dot, extract the first key */
-		key = getkey(&copy, sflags);
-		if (key == NULL)
-			return 0;
-
-		/* select the root item */
-		if (w->itf->sel(w->closure, key))
-			result = S_ok;
-		else if (key[0] == '*'
-		      && !key[1]
-		      && !value
-		      && !*copy
-		      && (w->flags & Mustach_With_ObjectIter)
-		      && w->itf->sel(w->closure, NULL))
-			result = S_ok_or_objiter;
-		else
-			result = S_none;
-		if (result == S_ok) {
-			/* iterate the selection of sub items */
-			key = getkey(&copy, sflags);
-			while(result == S_ok && key) {
-				if (w->itf->subsel(w->closure, key))
-					/* nothing */;
-				else if (key[0] == '*'
-				      && !key[1]
-				      && !value
-				      && !*copy
-				      && (w->flags & Mustach_With_ObjectIter))
-					result = S_objiter;
-				else
-					result = S_none;
-				key = getkey(&copy, sflags);
-			}
-		}
-	}
-	/* should it be compared? */
-	if (result == S_ok && value) {
-		if (!w->itf->compare)
-			result = S_none;
-		else {
-			i = value[0] == '!';
-			scmp = w->itf->compare(w->closure, &value[i]);
-			switch (k) {
-			case C_eq: j = scmp == 0; break;
-			case C_lt: j = scmp < 0; break;
-			case C_le: j = scmp <= 0; break;
-			case C_gt: j = scmp > 0; break;
-			case C_ge: j = scmp >= 0; break;
-			default: j = i; break;
-			}
-			if (i == j)
-				result = S_none;
-		}
-	}
-	return result;
-}
-
-static int start(void *closure)
-{
-	struct wrap *w = closure;
-	return w->itf->start ? w->itf->start(w->closure) : MUSTACH_OK;
-}
-
-static void stop(void *closure, int status)
-{
-	struct wrap *w = closure;
-	if (w->itf->stop)
-		w->itf->stop(w->closure, status);
-}
-
-static int write(struct wrap *w, const char *buffer, size_t size, FILE *file)
-{
-	int r;
-
-	if (w->writecb)
-		r = w->writecb(file, buffer, size);
-	else
-		r = fwrite(buffer, 1, size, file) == size ? MUSTACH_OK : MUSTACH_ERROR_SYSTEM;
-	return r;
-}
-
-static int emit(void *closure, const char *buffer, size_t size, int escape, FILE *file)
-{
-	struct wrap *w = closure;
-	int r;
-	size_t s, i;
-	char car;
-
-	if (w->emitcb)
-		r = w->emitcb(file, buffer, size, escape);
-	else if (!escape)
-		r = write(w, buffer, size, file);
-	else {
-		i = 0;
-		r = MUSTACH_OK;
-		while(i < size && r == MUSTACH_OK) {
-			s = i;
-			while (i < size && (car = buffer[i]) != '<' && car != '>' && car != '&' && car != '"')
-				i++;
-			if (i != s)
-				r = write(w, &buffer[s], i - s, file);
-			if (i < size && r == MUSTACH_OK) {
-				switch(car) {
-				case '<': r = write(w, "&lt;", 4, file); break;
-				case '>': r = write(w, "&gt;", 4, file); break;
-				case '&': r = write(w, "&amp;", 5, file); break;
-				case '"': r = write(w, "&quot;", 6, file); break;
-				}
-				i++;
-			}
-		}
-	}
-	return r;
-}
-
-static int enter(void *closure, const char *name)
-{
-	struct wrap *w = closure;
-	enum sel s = sel(w, name);
-	return s == S_none ? 0 : w->itf->enter(w->closure, s & S_objiter);
-}
-
-static int next(void *closure)
-{
-	struct wrap *w = closure;
-	return w->itf->next(w->closure);
-}
-
-static int leave(void *closure)
-{
-	struct wrap *w = closure;
-	return w->itf->leave(w->closure);
-}
-
-static int getoptional(struct wrap *w, const char *name, struct mustach_sbuf *sbuf)
-{
-	enum sel s = sel(w, name);
-	if (!(s & S_ok))
-		return 0;
-	return w->itf->get(w->closure, sbuf, s & S_objiter);
-}
-
-static int get(void *closure, const char *name, struct mustach_sbuf *sbuf)
-{
-	struct wrap *w = closure;
-	if (getoptional(w, name, sbuf) <= 0) {
-		if (w->flags & Mustach_With_ErrorUndefined)
-			return MUSTACH_ERROR_UNDEFINED_TAG;
-		sbuf->value = "";
-	}
-	return MUSTACH_OK;
-}
-
-static int get_partial_from_file(const char *name, struct mustach_sbuf *sbuf)
-{
-	static char extension[] = INCLUDE_PARTIAL_EXTENSION;
-	size_t s;
-	long pos;
-	FILE *file;
-	char *path, *buffer;
-
-	/* allocate path */
-	s = strlen(name);
-	path = malloc(s + sizeof extension);
-	if (path == NULL)
-		return MUSTACH_ERROR_SYSTEM;
-
-	/* try without extension first */
-	memcpy(path, name, s + 1);
-	file = fopen(path, "r");
-	if (file == NULL) {
-		memcpy(&path[s], extension, sizeof extension);
-		file = fopen(path, "r");
-	}
-	free(path);
-
-	/* if file opened */
-	if (file == NULL)
-		return MUSTACH_ERROR_PARTIAL_NOT_FOUND;
-
-	/* compute file size */
-	if (fseek(file, 0, SEEK_END) >= 0
-	 && (pos = ftell(file)) >= 0
-	 && fseek(file, 0, SEEK_SET) >= 0) {
-		/* allocate value */
-		s = (size_t)pos;
-		buffer = malloc(s + 1);
-		if (buffer != NULL) {
-			/* read value */
-			if (1 == fread(buffer, s, 1, file)) {
-				/* force zero at end */
-				sbuf->value = buffer;
-				buffer[s] = 0;
-				sbuf->freecb = free;
-				fclose(file);
-				return MUSTACH_OK;
-			}
-			free(buffer);
-		}
-	}
-	fclose(file);
-	return MUSTACH_ERROR_SYSTEM;
-}
-
-static int partial(void *closure, const char *name, struct mustach_sbuf *sbuf)
-{
-	struct wrap *w = closure;
-	int rc;
-	if (mustach_wrap_get_partial != NULL)
-		rc = mustach_wrap_get_partial(name, sbuf);
-	else if (w->flags & Mustach_With_PartialDataFirst) {
-		if (getoptional(w, name, sbuf) > 0)
-			rc = MUSTACH_OK;
-		else
-			rc = get_partial_from_file(name, sbuf);
-	}
-	else {
-		rc = get_partial_from_file(name, sbuf);
-		if (rc != MUSTACH_OK &&  getoptional(w, name, sbuf) > 0)
-			rc = MUSTACH_OK;
-	}
-	if (rc != MUSTACH_OK)
-		sbuf->value = "";
-	return MUSTACH_OK;
-}
-
-const struct mustach_itf mustach_wrap_itf = {
-	.start = start,
-	.put = NULL,
-	.enter = enter,
-	.next = next,
-	.leave = leave,
-	.partial = partial,
-	.get = get,
-	.emit = emit,
-	.stop = stop
-};
-
-static void wrap_init(struct wrap *wrap, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, mustach_write_cb_t *writecb)
-{
-	if (flags & Mustach_With_Compare)
-		flags |= Mustach_With_Equal;
-	wrap->closure = closure;
-	wrap->itf = itf;
-	wrap->flags = flags;
-	wrap->emitcb = emitcb;
-	wrap->writecb = writecb;
-}
-
-int mustach_wrap_file(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, FILE *file)
-{
-	struct wrap w;
-	wrap_init(&w, itf, closure, flags, NULL, NULL);
-	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, file);
-}
-
-int mustach_wrap_fd(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, int fd)
-{
-	struct wrap w;
-	wrap_init(&w, itf, closure, flags, NULL, NULL);
-	return mustach_fd(template, length, &mustach_wrap_itf, &w, flags, fd);
-}
-
-int mustach_wrap_mem(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, char **result, size_t *size)
-{
-	struct wrap w;
-	wrap_init(&w, itf, closure, flags, NULL, NULL);
-	return mustach_mem(template, length, &mustach_wrap_itf, &w, flags, result, size);
-}
-
-int mustach_wrap_write(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_write_cb_t *writecb, void *writeclosure)
-{
-	struct wrap w;
-	wrap_init(&w, itf, closure, flags, NULL, writecb);
-	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, writeclosure);
-}
-
-int mustach_wrap_emit(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, void *emitclosure)
-{
-	struct wrap w;
-	wrap_init(&w, itf, closure, flags, emitcb, NULL);
-	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, emitclosure);
-}
-
--- a/extern/libmustach/mustach-wrap.h	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,234 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#ifndef _mustach_wrap_h_included_
-#define _mustach_wrap_h_included_
-
-/*
- * mustach-wrap is intended to make integration of JSON
- * libraries easier by wrapping mustach extensions in a
- * single place.
- *
- * As before, using mustach and only mustach is possible
- * (by using only mustach.h) but does not implement high
- * level features coming with extensions implemented by
- * this high level wrapper.
- */
-#include "mustach.h"
-/*
- * Definition of the writing callbacks for mustach functions
- * producing output to callbacks.
- *
- * Two callback types are defined:
- *
- * @mustach_write_cb_t:
- *
- *    callback receiving the escaped data to be written as 3 parameters:
- *
- *    1. the 'closure', the same given to the wmustach_... function
- *    2. a pointer to a 'buffer' containing the characters to be written
- *    3. the size in bytes of the data pointed by 'buffer'
- *
- * @mustach_emit_cb_t:
- *
- *    callback receiving the data to be written and a flag indicating
- *    if escaping should be done or not as 4 parameters:
- *
- *    1. the 'closure', the same given to the emustach_... function
- *    2. a pointer to a 'buffer' containing the characters to be written
- *    3. the size in bytes of the data pointed by 'buffer'
- *    4. a boolean indicating if 'escape' should be done
- */
-#ifndef _mustach_output_callbacks_defined_
-#define _mustach_output_callbacks_defined_
-typedef int mustach_write_cb_t(void *closure, const char *buffer, size_t size);
-typedef int mustach_emit_cb_t(void *closure, const char *buffer, size_t size, int escape);
-#endif
-
-/**
- * Flags specific to mustach wrap
- */
-#define Mustach_With_SingleDot            4     /* obsolete, always set */
-#define Mustach_With_Equal                8
-#define Mustach_With_Compare             16
-#define Mustach_With_JsonPointer         32
-#define Mustach_With_ObjectIter          64
-#define Mustach_With_IncPartial         128     /* obsolete, always set */
-#define Mustach_With_EscFirstCmp        256
-#define Mustach_With_PartialDataFirst   512
-#define Mustach_With_ErrorUndefined    1024
-
-#undef  Mustach_With_AllExtensions
-#define Mustach_With_AllExtensions     1023     /* don't include ErrorUndefined */
-
-/**
- * mustach_wrap_itf - high level wrap of mustach - interface for callbacks
- *
- * The functions sel, subsel, enter and next should return 0 or 1.
- *
- * All other functions should normally return MUSTACH_OK (zero).
- *
- * If any function returns a negative value, it means an error that
- * stop the processing and that is reported to the caller. Mustach
- * also has its own error codes. Using the macros MUSTACH_ERROR_USER
- * and MUSTACH_IS_ERROR_USER could help to avoid clashes.
- *
- * @start: If defined (can be NULL), starts the mustach processing
- *         of the closure, called at the very beginning before any
- *         mustach processing occurs.
- *
- * @stop: If defined (can be NULL), stops the mustach processing
- *        of the closure, called at the very end after all mustach
- *        processing occurered. The status returned by the processing
- *        is passed to the stop.
- *
- * @compare: If defined (can be NULL), compares the value of the
- *           currently selected item with the given value and returns
- *           a negative value if current value is lesser, a positive
- *           value if the current value is greater or zero when
- *           values are equals.
- *           If 'compare' is NULL, any comparison in mustach
- *           is going to fails.
- *
- * @sel: Selects the item of the given 'name'. If 'name' is NULL
- *       Selects the current item. Returns 1 if the selection is
- *       effective or else 0 if the selection failed.
- *
- * @subsel: Selects from the currently selected object the value of
- *          the field of given name. Returns 1 if the selection is
- *          effective or else 0 if the selection failed.
- *
- * @enter: Enters the section of 'name' if possible.
- *         Musts return 1 if entered or 0 if not entered.
- *         When 1 is returned, the function 'leave' will always be called.
- *         Conversely 'leave' is never called when enter returns 0 or
- *         a negative value.
- *         When 1 is returned, the function must activate the first
- *         item of the section.
- *
- * @next: Activates the next item of the section if it exists.
- *        Musts return 1 when the next item is activated.
- *        Musts return 0 when there is no item to activate.
- *
- * @leave: Leaves the last entered section
- *
- * @get: Returns in 'sbuf' the value of the current selection if 'key'
- *       is zero. Otherwise, when 'key' is not zero, return in 'sbuf'
- *       the name of key of the current selection, or if no such key
- *       exists, the empty string. Must return 1 if possible or
- *       0 when not possible or an error code.
- */
-struct mustach_wrap_itf {
-	int (*start)(void *closure);
-	void (*stop)(void *closure, int status);
-	int (*compare)(void *closure, const char *value);
-	int (*sel)(void *closure, const char *name);
-	int (*subsel)(void *closure, const char *name);
-	int (*enter)(void *closure, int objiter);
-	int (*next)(void *closure);
-	int (*leave)(void *closure);
-	int (*get)(void *closure, struct mustach_sbuf *sbuf, int key);
-};
-
-/**
- * Mustach interface used internally by mustach wrapper functions.
- * Can be used for overriding behaviour.
- */
-extern const struct mustach_itf mustach_wrap_itf;
-
-/**
- * Global hook for providing partials. When set to a not NULL value, the pointed
- * function replaces the default behaviour and is called to provide the partial
- * of the given 'name' in 'sbuf'.
- * The function must return MUSTACH_OK when it filled 'sbuf' with value of partial
- * or must return an error code if it failed.
- */
-extern int (*mustach_wrap_get_partial)(const char *name, struct mustach_sbuf *sbuf);
-
-/**
- * mustach_wrap_file - Renders the mustache 'template' in 'file' for an abstract
- * wrapper of interface 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface of the abstract wrapper
- * @closure:  the closure of the abstract wrapper
- * @file:     the file where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_wrap_file(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, FILE *file);
-
-/**
- * mustach_wrap_fd - Renders the mustache 'template' in 'fd' for an abstract
- * wrapper of interface 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface of the abstract wrapper
- * @closure:  the closure of the abstract wrapper
- * @fd:       the file descriptor number where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_wrap_fd(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, int fd);
-
-/**
- * mustach_wrap_mem - Renders the mustache 'template' in 'result' for an abstract
- * wrapper of interface 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface of the abstract wrapper
- * @closure:  the closure of the abstract wrapper
- * @result:   the pointer receiving the result when 0 is returned
- * @size:     the size of the returned result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_wrap_mem(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, char **result, size_t *size);
-
-/**
- * mustach_wrap_write - Renders the mustache 'template' for an abstract
- * wrapper of interface 'itf' and 'closure' to custom writer
- * 'writecb' with 'writeclosure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface of the abstract wrapper
- * @closure:  the closure of the abstract wrapper
- * @writecb:  the function that write values
- * @closure:  the closure for the write function
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_wrap_write(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_write_cb_t *writecb, void *writeclosure);
-
-/**
- * mustach_wrap_emit - Renders the mustache 'template' for an abstract
- * wrapper of interface 'itf' and 'closure' to custom emiter 'emitcb'
- * with 'emitclosure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface of the abstract wrapper
- * @closure:  the closure of the abstract wrapper
- * @emitcb:   the function that emit values
- * @closure:  the closure for the write function
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_wrap_emit(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, void *emitclosure);
-
-#endif
-
--- a/extern/libmustach/mustach.c	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,546 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#define _GNU_SOURCE
-
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-#include <errno.h>
-#include <ctype.h>
-#ifdef _WIN32
-#include <malloc.h>
-#endif
-
-#include "mustach.h"
-
-struct iwrap {
-	int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file);
-	void *closure; /* closure for: enter, next, leave, emit, get */
-	int (*put)(void *closure, const char *name, int escape, FILE *file);
-	void *closure_put; /* closure for put */
-	int (*enter)(void *closure, const char *name);
-	int (*next)(void *closure);
-	int (*leave)(void *closure);
-	int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf);
-	int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf);
-	void *closure_partial; /* closure for partial */
-	int flags;
-};
-
-struct prefix {
-	size_t len;
-	const char *start;
-	struct prefix *prefix;
-};
-
-#if !defined(NO_OPEN_MEMSTREAM)
-static FILE *memfile_open(char **buffer, size_t *size)
-{
-	return open_memstream(buffer, size);
-}
-static void memfile_abort(FILE *file, char **buffer, size_t *size)
-{
-	fclose(file);
-	free(*buffer);
-	*buffer = NULL;
-	*size = 0;
-}
-static int memfile_close(FILE *file, char **buffer, size_t *size)
-{
-	int rc;
-
-	/* adds terminating null */
-	rc = fputc(0, file) ? MUSTACH_ERROR_SYSTEM : 0;
-	fclose(file);
-	if (rc == 0)
-		/* removes terminating null of the length */
-		(*size)--;
-	else {
-		free(*buffer);
-		*buffer = NULL;
-		*size = 0;
-	}
-	return rc;
-}
-#else
-static FILE *memfile_open(char **buffer, size_t *size)
-{
-	/*
-	 * We can't provide *buffer and *size as open_memstream does but
-	 * at least clear them so the caller won't get bad data.
-	 */
-	*buffer = NULL;
-	*size = 0;
-
-	return tmpfile();
-}
-static void memfile_abort(FILE *file, char **buffer, size_t *size)
-{
-	fclose(file);
-	*buffer = NULL;
-	*size = 0;
-}
-static int memfile_close(FILE *file, char **buffer, size_t *size)
-{
-	int rc;
-	size_t s;
-	char *b;
-
-	s = (size_t)ftell(file);
-	b = malloc(s + 1);
-	if (b == NULL) {
-		rc = MUSTACH_ERROR_SYSTEM;
-		errno = ENOMEM;
-		s = 0;
-	} else {
-		rewind(file);
-		if (1 == fread(b, s, 1, file)) {
-			rc = 0;
-			b[s] = 0;
-		} else {
-			rc = MUSTACH_ERROR_SYSTEM;
-			free(b);
-			b = NULL;
-			s = 0;
-		}
-	}
-	*buffer = b;
-	*size = s;
-	return rc;
-}
-#endif
-
-static inline void sbuf_reset(struct mustach_sbuf *sbuf)
-{
-	sbuf->value = NULL;
-	sbuf->freecb = NULL;
-	sbuf->closure = NULL;
-	sbuf->length = 0;
-}
-
-static inline void sbuf_release(struct mustach_sbuf *sbuf)
-{
-	if (sbuf->releasecb)
-		sbuf->releasecb(sbuf->value, sbuf->closure);
-}
-
-static inline size_t sbuf_length(struct mustach_sbuf *sbuf)
-{
-	size_t length = sbuf->length;
-	if (length == 0 && sbuf->value != NULL)
-		length = strlen(sbuf->value);
-	return length;
-}
-
-static int iwrap_emit(void *closure, const char *buffer, size_t size, int escape, FILE *file)
-{
-	size_t i, j, r;
-
-	(void)closure; /* unused */
-
-	if (!escape)
-		return fwrite(buffer, 1, size, file) != size ? MUSTACH_ERROR_SYSTEM : MUSTACH_OK;
-
-	r = i = 0;
-	while (i < size) {
-		j = i;
-		while (j < size && buffer[j] != '<' && buffer[j] != '>' && buffer[j] != '&' && buffer[j] != '"')
-			j++;
-		if (j != i && fwrite(&buffer[i], j - i, 1, file) != 1)
-			return MUSTACH_ERROR_SYSTEM;
-		if (j < size) {
-			switch(buffer[j++]) {
-			case '<':
-				r = fwrite("&lt;", 4, 1, file);
-				break;
-			case '>':
-				r = fwrite("&gt;", 4, 1, file);
-				break;
-			case '&':
-				r = fwrite("&amp;", 5, 1, file);
-				break;
-			case '"':
-				r = fwrite("&quot;", 6, 1, file);
-				break;
-			}
-			if (r != 1)
-				return MUSTACH_ERROR_SYSTEM;
-		}
-		i = j;
-	}
-	return MUSTACH_OK;
-}
-
-static int iwrap_put(void *closure, const char *name, int escape, FILE *file)
-{
-	struct iwrap *iwrap = closure;
-	int rc;
-	struct mustach_sbuf sbuf;
-	size_t length;
-
-	sbuf_reset(&sbuf);
-	rc = iwrap->get(iwrap->closure, name, &sbuf);
-	if (rc >= 0) {
-		length = sbuf_length(&sbuf);
-		if (length)
-			rc = iwrap->emit(iwrap->closure, sbuf.value, length, escape, file);
-		sbuf_release(&sbuf);
-	}
-	return rc;
-}
-
-static int iwrap_partial(void *closure, const char *name, struct mustach_sbuf *sbuf)
-{
-	struct iwrap *iwrap = closure;
-	int rc;
-	FILE *file;
-	size_t size;
-	char *result;
-
-	result = NULL;
-	file = memfile_open(&result, &size);
-	if (file == NULL)
-		rc = MUSTACH_ERROR_SYSTEM;
-	else {
-		rc = iwrap->put(iwrap->closure_put, name, 0, file);
-		if (rc < 0)
-			memfile_abort(file, &result, &size);
-		else {
-			rc = memfile_close(file, &result, &size);
-			if (rc == 0) {
-				sbuf->value = result;
-				sbuf->freecb = free;
-				sbuf->length = size;
-			}
-		}
-	}
-	return rc;
-}
-
-static int emitprefix(struct iwrap *iwrap, FILE *file, struct prefix *prefix)
-{
-	if (prefix->prefix) {
-		int rc = emitprefix(iwrap, file, prefix->prefix);
-		if (rc < 0)
-			return rc;
-	}
-	return prefix->len ? iwrap->emit(iwrap->closure, prefix->start, prefix->len, 0, file) : 0;
-}
-
-static int process(const char *template, size_t length, struct iwrap *iwrap, FILE *file, struct prefix *prefix)
-{
-	struct mustach_sbuf sbuf;
-	char opstr[MUSTACH_MAX_DELIM_LENGTH], clstr[MUSTACH_MAX_DELIM_LENGTH];
-	char name[MUSTACH_MAX_LENGTH + 1], c;
-	const char *beg, *term, *end;
-	struct { const char *name, *again; size_t length; unsigned enabled: 1, entered: 1; } stack[MUSTACH_MAX_DEPTH];
-	size_t oplen, cllen, len, l;
-	int depth, rc, enabled, stdalone;
-	struct prefix pref;
-
-	pref.prefix = prefix;
-	end = template + (length ? length : strlen(template));
-	opstr[0] = opstr[1] = '{';
-	clstr[0] = clstr[1] = '}';
-	oplen = cllen = 2;
-	stdalone = enabled = 1;
-	depth = pref.len = 0;
-	for (;;) {
-		/* search next openning delimiter */
-		for (beg = template ; ; beg++) {
-			c = beg == end ? '\n' : *beg;
-			if (c == '\n') {
-				l = (beg != end) + (size_t)(beg - template);
-				if (stdalone != 2 && enabled) {
-					if (beg != template /* don't prefix empty lines */) {
-						rc = emitprefix(iwrap, file, &pref);
-						if (rc < 0)
-							return rc;
-					}
-					rc = iwrap->emit(iwrap->closure, template, l, 0, file);
-					if (rc < 0)
-						return rc;
-				}
-				if (beg == end) /* no more mustach */
-					return depth ? MUSTACH_ERROR_UNEXPECTED_END : MUSTACH_OK;
-				template += l;
-				stdalone = 1;
-				pref.len = 0;
-			}
-			else if (!isspace(c)) {
-				if (stdalone == 2 && enabled) {
-					rc = emitprefix(iwrap, file, &pref);
-					if (rc < 0)
-						return rc;
-					pref.len = 0;
-					stdalone = 0;
-				}
-				if (c == *opstr && end - beg >= (ssize_t)oplen) {
-					for (l = 1 ; l < oplen && beg[l] == opstr[l] ; l++);
-					if (l == oplen)
-						break;
-				}
-				stdalone = 0;
-			}
-		}
-
-		pref.start = template;
-		pref.len = enabled ? (size_t)(beg - template) : 0;
-		beg += oplen;
-
-		/* search next closing delimiter */
-		for (term = beg ; ; term++) {
-			if (term == end)
-				return MUSTACH_ERROR_UNEXPECTED_END;
-			if (*term == *clstr && end - term >= (ssize_t)cllen) {
-				for (l = 1 ; l < cllen && term[l] == clstr[l] ; l++);
-				if (l == cllen)
-					break;
-			}
-		}
-		template = term + cllen;
-		len = (size_t)(term - beg);
-		c = *beg;
-		switch(c) {
-		case ':':
-			stdalone = 0;
-			if (iwrap->flags & Mustach_With_Colon)
-				goto exclude_first;
-			goto get_name;
-		case '!':
-		case '=':
-			break;
-		case '{':
-			for (l = 0 ; l < cllen && clstr[l] == '}' ; l++);
-			if (l < cllen) {
-				if (!len || beg[len-1] != '}')
-					return MUSTACH_ERROR_BAD_UNESCAPE_TAG;
-				len--;
-			} else {
-				if (term[l] != '}')
-					return MUSTACH_ERROR_BAD_UNESCAPE_TAG;
-				template++;
-			}
-			c = '&';
-			/*@fallthrough@*/
-		case '&':
-			stdalone = 0;
-			/*@fallthrough@*/
-		case '^':
-		case '#':
-		case '/':
-		case '>':
-exclude_first:
-			beg++;
-			len--;
-			goto get_name;
-		default:
-			stdalone = 0;
-get_name:
-			while (len && isspace(beg[0])) { beg++; len--; }
-			while (len && isspace(beg[len-1])) len--;
-			if (len == 0 && !(iwrap->flags & Mustach_With_EmptyTag))
-				return MUSTACH_ERROR_EMPTY_TAG;
-			if (len > MUSTACH_MAX_LENGTH)
-				return MUSTACH_ERROR_TAG_TOO_LONG;
-			memcpy(name, beg, len);
-			name[len] = 0;
-			break;
-		}
-		if (stdalone)
-			stdalone = 2;
-		else if (enabled) {
-			rc = emitprefix(iwrap, file, &pref);
-			if (rc < 0)
-				return rc;
-			pref.len = 0;
-		}
-		switch(c) {
-		case '!':
-			/* comment */
-			/* nothing to do */
-			break;
-		case '=':
-			/* defines delimiters */
-			if (len < 5 || beg[len - 1] != '=')
-				return MUSTACH_ERROR_BAD_SEPARATORS;
-			beg++;
-			len -= 2;
-			while (len && isspace(*beg))
-				beg++, len--;
-			while (len && isspace(beg[len - 1]))
-				len--;
-			for (l = 0; l < len && !isspace(beg[l]) ; l++);
-			if (l == len || l > MUSTACH_MAX_DELIM_LENGTH)
-				return MUSTACH_ERROR_BAD_SEPARATORS;
-			oplen = l;
-			memcpy(opstr, beg, l);
-			while (l < len && isspace(beg[l])) l++;
-			if (l == len || len - l > MUSTACH_MAX_DELIM_LENGTH)
-				return MUSTACH_ERROR_BAD_SEPARATORS;
-			cllen = len - l;
-			memcpy(clstr, beg + l, cllen);
-			break;
-		case '^':
-		case '#':
-			/* begin section */
-			if (depth == MUSTACH_MAX_DEPTH)
-				return MUSTACH_ERROR_TOO_DEEP;
-			rc = enabled;
-			if (rc) {
-				rc = iwrap->enter(iwrap->closure, name);
-				if (rc < 0)
-					return rc;
-			}
-			stack[depth].name = beg;
-			stack[depth].again = template;
-			stack[depth].length = len;
-			stack[depth].enabled = enabled != 0;
-			stack[depth].entered = rc != 0;
-			if ((c == '#') == (rc == 0))
-				enabled = 0;
-			depth++;
-			break;
-		case '/':
-			/* end section */
-			if (depth-- == 0 || len != stack[depth].length || memcmp(stack[depth].name, name, len))
-				return MUSTACH_ERROR_CLOSING;
-			rc = enabled && stack[depth].entered ? iwrap->next(iwrap->closure) : 0;
-			if (rc < 0)
-				return rc;
-			if (rc) {
-				template = stack[depth++].again;
-			} else {
-				enabled = stack[depth].enabled;
-				if (enabled && stack[depth].entered)
-					iwrap->leave(iwrap->closure);
-			}
-			break;
-		case '>':
-			/* partials */
-			if (enabled) {
-				sbuf_reset(&sbuf);
-				rc = iwrap->partial(iwrap->closure_partial, name, &sbuf);
-				if (rc >= 0) {
-					rc = process(sbuf.value, sbuf_length(&sbuf), iwrap, file, &pref);
-					sbuf_release(&sbuf);
-				}
-				if (rc < 0)
-					return rc;
-			}
-			break;
-		default:
-			/* replacement */
-			if (enabled) {
-				rc = iwrap->put(iwrap->closure_put, name, c != '&', file);
-				if (rc < 0)
-					return rc;
-			}
-			break;
-		}
-	}
-}
-
-int mustach_file(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, FILE *file)
-{
-	int rc;
-	struct iwrap iwrap;
-
-	/* check validity */
-	if (!itf->enter || !itf->next || !itf->leave || (!itf->put && !itf->get))
-		return MUSTACH_ERROR_INVALID_ITF;
-
-	/* init wrap structure */
-	iwrap.closure = closure;
-	if (itf->put) {
-		iwrap.put = itf->put;
-		iwrap.closure_put = closure;
-	} else {
-		iwrap.put = iwrap_put;
-		iwrap.closure_put = &iwrap;
-	}
-	if (itf->partial) {
-		iwrap.partial = itf->partial;
-		iwrap.closure_partial = closure;
-	} else if (itf->get) {
-		iwrap.partial = itf->get;
-		iwrap.closure_partial = closure;
-	} else {
-		iwrap.partial = iwrap_partial;
-		iwrap.closure_partial = &iwrap;
-	}
-	iwrap.emit = itf->emit ? itf->emit : iwrap_emit;
-	iwrap.enter = itf->enter;
-	iwrap.next = itf->next;
-	iwrap.leave = itf->leave;
-	iwrap.get = itf->get;
-	iwrap.flags = flags;
-
-	/* process */
-	rc = itf->start ? itf->start(closure) : 0;
-	if (rc == 0)
-		rc = process(template, length, &iwrap, file, 0);
-	if (itf->stop)
-		itf->stop(closure, rc);
-	return rc;
-}
-
-int mustach_fd(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, int fd)
-{
-	int rc;
-	FILE *file;
-
-	file = fdopen(fd, "w");
-	if (file == NULL) {
-		rc = MUSTACH_ERROR_SYSTEM;
-		errno = ENOMEM;
-	} else {
-		rc = mustach_file(template, length, itf, closure, flags, file);
-		fclose(file);
-	}
-	return rc;
-}
-
-int mustach_mem(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, char **result, size_t *size)
-{
-	int rc;
-	FILE *file;
-	size_t s;
-
-	*result = NULL;
-	if (size == NULL)
-		size = &s;
-	file = memfile_open(result, size);
-	if (file == NULL)
-		rc = MUSTACH_ERROR_SYSTEM;
-	else {
-		rc = mustach_file(template, length, itf, closure, flags, file);
-		if (rc < 0)
-			memfile_abort(file, result, size);
-		else
-			rc = memfile_close(file, result, size);
-	}
-	return rc;
-}
-
-int fmustach(const char *template, const struct mustach_itf *itf, void *closure, FILE *file)
-{
-	return mustach_file(template, 0, itf, closure, Mustach_With_AllExtensions, file);
-}
-
-int fdmustach(const char *template, const struct mustach_itf *itf, void *closure, int fd)
-{
-	return mustach_fd(template, 0, itf, closure, Mustach_With_AllExtensions, fd);
-}
-
-int mustach(const char *template, const struct mustach_itf *itf, void *closure, char **result, size_t *size)
-{
-	return mustach_mem(template, 0, itf, closure, Mustach_With_AllExtensions, result, size);
-}
-
--- a/extern/libmustach/mustach.h	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,313 +0,0 @@
-/*
- Author: José Bollo <jobol@nonadev.net>
-
- https://gitlab.com/jobol/mustach
-
- SPDX-License-Identifier: ISC
-*/
-
-#ifndef _mustach_h_included_
-#define _mustach_h_included_
-
-struct mustach_sbuf; /* see below */
-
-/**
- * Current version of mustach and its derivates
- */
-#define MUSTACH_VERSION 102
-#define MUSTACH_VERSION_MAJOR (MUSTACH_VERSION / 100)
-#define MUSTACH_VERSION_MINOR (MUSTACH_VERSION % 100)
-
-/**
- * Maximum nested imbrications supported
- */
-#define MUSTACH_MAX_DEPTH  256
-
-/**
- * Maximum length of tags in mustaches {{...}}
- */
-#define MUSTACH_MAX_LENGTH 4096
-
-/**
- * Maximum length of delimitors (2 normally but extended here)
- */
-#define MUSTACH_MAX_DELIM_LENGTH 8
-
-/**
- * Flags specific to mustach core
- */
-#define Mustach_With_NoExtensions   0
-#define Mustach_With_Colon          1
-#define Mustach_With_EmptyTag       2
-#define Mustach_With_AllExtensions  3
-
-/*
- * Definition of error codes returned by mustach
- */
-#define MUSTACH_OK                       0
-#define MUSTACH_ERROR_SYSTEM            -1
-#define MUSTACH_ERROR_UNEXPECTED_END    -2
-#define MUSTACH_ERROR_EMPTY_TAG         -3
-#define MUSTACH_ERROR_TAG_TOO_LONG      -4
-#define MUSTACH_ERROR_BAD_SEPARATORS    -5
-#define MUSTACH_ERROR_TOO_DEEP          -6
-#define MUSTACH_ERROR_CLOSING           -7
-#define MUSTACH_ERROR_BAD_UNESCAPE_TAG  -8
-#define MUSTACH_ERROR_INVALID_ITF       -9
-#define MUSTACH_ERROR_ITEM_NOT_FOUND    -10
-#define MUSTACH_ERROR_PARTIAL_NOT_FOUND -11
-#define MUSTACH_ERROR_UNDEFINED_TAG     -12
-
-/*
- * You can use definition below for user specific error
- *
- * The macro MUSTACH_ERROR_USER is involutive so for any value
- *   value = MUSTACH_ERROR_USER(MUSTACH_ERROR_USER(value))
- */
-#define MUSTACH_ERROR_USER_BASE         -100
-#define MUSTACH_ERROR_USER(x)           (MUSTACH_ERROR_USER_BASE-(x))
-#define MUSTACH_IS_ERROR_USER(x)        (MUSTACH_ERROR_USER(x) >= 0)
-
-/**
- * mustach_itf - pure abstract mustach - interface for callbacks
- *
- * The functions enter and next should return 0 or 1.
- *
- * All other functions should normally return MUSTACH_OK (zero).
- *
- * If any function returns a negative value, it means an error that
- * stop the processing and that is reported to the caller. Mustach
- * also has its own error codes. Using the macros MUSTACH_ERROR_USER
- * and MUSTACH_IS_ERROR_USER could help to avoid clashes.
- *
- * @start: If defined (can be NULL), starts the mustach processing
- *         of the closure, called at the very beginning before any
- *         mustach processing occurs.
- *
- * @put: If defined (can be NULL), writes the value of 'name'
- *       to 'file' with 'escape' or not.
- *       As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be
- *       the empty string. In that later case an implementation can
- *       return MUSTACH_ERROR_EMPTY_TAG to refuse empty names.
- *       If NULL and 'get' NULL the error MUSTACH_ERROR_INVALID_ITF
- *       is returned.
- *
- * @enter: Enters the section of 'name' if possible.
- *         Musts return 1 if entered or 0 if not entered.
- *         When 1 is returned, the function 'leave' will always be called.
- *         Conversely 'leave' is never called when enter returns 0 or
- *         a negative value.
- *         When 1 is returned, the function must activate the first
- *         item of the section.
- *
- * @next: Activates the next item of the section if it exists.
- *        Musts return 1 when the next item is activated.
- *        Musts return 0 when there is no item to activate.
- *
- * @leave: Leaves the last entered section
- *
- * @partial: If defined (can be NULL), returns in 'sbuf' the content of the
- *           partial of 'name'. @see mustach_sbuf
- *           If NULL but 'get' not NULL, 'get' is used instead of partial.
- *           If NULL and 'get' NULL and 'put' not NULL, 'put' is called with
- *           a true FILE.
- *
- * @emit: If defined (can be NULL), writes the 'buffer' of 'size' with 'escape'.
- *        If NULL the standard function 'fwrite' is used with a true FILE.
- *        If not NULL that function is called instead of 'fwrite' to output
- *        text.
- *        It implies that if you define either 'partial' or 'get' callback,
- *        the meaning of 'FILE *file' is abstract for mustach's process and
- *        then you can use 'FILE*file' pass any kind of pointer (including NULL)
- *        to the function 'fmustach'. An example of a such behaviour is given by
- *        the implementation of 'mustach_json_c_write'.
- *
- * @get: If defined (can be NULL), returns in 'sbuf' the value of 'name'.
- *       As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be
- *       the empty string. In that later case an implementation can
- *       return MUSTACH_ERROR_EMPTY_TAG to refuse empty names.
- *       If 'get' is NULL and 'put' NULL the error MUSTACH_ERROR_INVALID_ITF
- *       is returned.
- *
- * @stop: If defined (can be NULL), stops the mustach processing
- *        of the closure, called at the very end after all mustach
- *        processing occurered. The status returned by the processing
- *        is passed to the stop.
- *
- * The array below summarize status of callbacks:
- *
- *    FULLY OPTIONAL:   start partial
- *    MANDATORY:        enter next leave
- *    COMBINATORIAL:    put emit get
- *
- * Not definig a MANDATORY callback returns error MUSTACH_ERROR_INVALID_ITF.
- *
- * For COMBINATORIAL callbacks the array below summarize possible combinations:
- *
- *  combination  : put     : emit    : get     : abstract FILE
- *  -------------+---------+---------+---------+-----------------------
- *  HISTORIC     : defined : NULL    : NULL    : NO: standard FILE
- *  MINIMAL      : NULL    : NULL    : defined : NO: standard FILE
- *  CUSTOM       : NULL    : defined : defined : YES: abstract FILE
- *  DUCK         : defined : NULL    : defined : NO: standard FILE
- *  DANGEROUS    : defined : defined : any     : YES or NO, depends on 'partial'
- *  INVALID      : NULL    : any     : NULL    : -
- *
- * The DUCK case runs on one leg. 'get' is not used if 'partial' is defined
- * but is used for 'partial' if 'partial' is NULL. Thus for clarity, do not use
- * it that way but define 'partial' and let 'get' be NULL.
- *
- * The DANGEROUS case is special: it allows abstract FILE if 'partial' is defined
- * but forbids abstract FILE when 'partial' is NULL.
- *
- * The INVALID case returns error MUSTACH_ERROR_INVALID_ITF.
- */
-struct mustach_itf {
-	int (*start)(void *closure);
-	int (*put)(void *closure, const char *name, int escape, FILE *file);
-	int (*enter)(void *closure, const char *name);
-	int (*next)(void *closure);
-	int (*leave)(void *closure);
-	int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf);
-	int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file);
-	int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf);
-	void (*stop)(void *closure, int status);
-};
-
-/**
- * mustach_sbuf - Interface for handling zero terminated strings
- *
- * That structure is used for returning zero terminated strings -in 'value'-
- * to mustach. The callee can provide a function for releasing the returned
- * 'value'. Three methods for releasing the string are possible.
- *
- *  1. no release: set either 'freecb' or 'releasecb' with NULL (done by default)
- *  2. release without closure: set 'freecb' to its expected value
- *  3. release with closure: set 'releasecb' and 'closure' to their expected values
- *
- * @value: The value of the string. That value is not changed by mustach -const-.
- *
- * @freecb: The function to call for freeing the value without closure.
- *          For convenience, signature of that callback is compatible with 'free'.
- *          Can be NULL.
- *
- * @releasecb: The function to release with closure.
- *             Can be NULL.
- *
- * @closure: The closure to use for 'releasecb'.
- *
- * @length: Length of the value or zero if unknown and value null terminated.
- *          To return the empty string, let it to zero and let value to NULL.
- */
-struct mustach_sbuf {
-	const char *value;
-	union {
-		void (*freecb)(void*);
-		void (*releasecb)(const char *value, void *closure);
-	};
-	void *closure;
-	size_t length;
-};
-
-/**
- * mustach_file - Renders the mustache 'template' in 'file' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @file:     the file where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_file(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, FILE *file);
-
-/**
- * mustach_fd - Renders the mustache 'template' in 'fd' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @fd:       the file descriptor number where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_fd(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, int fd);
-
-/**
- * mustach_mem - Renders the mustache 'template' in 'result' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate
- * @length:   length of the template or zero if unknown and template null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @result:   the pointer receiving the result when 0 is returned
- * @size:     the size of the returned result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-extern int mustach_mem(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, char **result, size_t *size);
-
-/***************************************************************************
-* compatibility with version before 1.0
-*/
-#ifdef __GNUC__
-#define DEPRECATED_MUSTACH(func) func __attribute__ ((deprecated))
-#elif defined(_MSC_VER)
-#define DEPRECATED_MUSTACH(func) __declspec(deprecated) func
-#elif !defined(DEPRECATED_MUSTACH)
-#pragma message("WARNING: You need to implement DEPRECATED_MUSTACH for this compiler")
-#define DEPRECATED_MUSTACH(func) func
-#endif
-/**
- * OBSOLETE use mustach_file
- *
- * fmustach - Renders the mustache 'template' in 'file' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate, null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @file:     the file where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-DEPRECATED_MUSTACH(extern int fmustach(const char *template, const struct mustach_itf *itf, void *closure, FILE *file));
-
-/**
- * OBSOLETE use mustach_fd
- *
- * fdmustach - Renders the mustache 'template' in 'fd' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate, null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @fd:       the file descriptor number where to write the result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-DEPRECATED_MUSTACH(extern int fdmustach(const char *template, const struct mustach_itf *itf, void *closure, int fd));
-
-/**
- * OBSOLETE use mustach_mem
- *
- * mustach - Renders the mustache 'template' in 'result' for 'itf' and 'closure'.
- *
- * @template: the template string to instantiate, null terminated
- * @itf:      the interface to the functions that mustach calls
- * @closure:  the closure to pass to functions called
- * @result:   the pointer receiving the result when 0 is returned
- * @size:     the size of the returned result
- *
- * Returns 0 in case of success, -1 with errno set in case of system error
- * a other negative value in case of error.
- */
-DEPRECATED_MUSTACH(extern int mustach(const char *template, const struct mustach_itf *itf, void *closure, char **result, size_t *size));
-
-#endif
-
--- a/html/header.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/header.html	Fri Mar 17 07:43:20 2023 +0100
@@ -4,7 +4,7 @@
 		<meta charset="UTF-8">
 		<meta name="viewport" content="width=device-width, initial-scale=1">
 		<link rel="stylesheet" href="/static/style.css">
-		<title>{{title}}</title>
+		<title>@@title@@</title>
 	</head>
 
 	<body>
--- a/html/index.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/index.html	Fri Mar 17 07:43:20 2023 +0100
@@ -11,14 +11,6 @@
 					<tr>
 				</thead>
 				<tbody>
-					{{#pastes}}
-					<tr>
-						<td><a href="/paste/{{id}}">{{title}}</a></td>
-						<td>{{author}}</td>
-						<td>{{language}}</td>
-						<td>{{date}}</td>
-						<td>{{expiration}}</td>
-					</tr>
-					{{/pastes}}
+					@@pastes@@
 				</tbody>
 			</table>
--- a/html/new.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/new.html	Fri Mar 17 07:43:20 2023 +0100
@@ -4,21 +4,19 @@
 				<table>
 					<tr>
 						<td class="label">Title</td>
-						<td><input name="title" type="text" placeholder="Untitled" value="{{title}}" /></td>
+						<td><input name="title" type="text" placeholder="Untitled" value="@@title@@" /></td>
 					</tr>
 
 					<tr>
 						<td class="label">Author</td>
-						<td><input name="author" type="text" placeholder="Anonymous" value="{{author}}" /></td>
+						<td><input name="author" type="text" placeholder="Anonymous" value="@@author@@" /></td>
 					</tr>
 
 					<tr>
 						<td class="label">Language</td>
 						<td>
 							<select name="language">
-								{{#languages}}
-								<option value="{{name}}" {{selected}}>{{name}}</option>
-								{{/languages}}
+								@@languages@@
 							</select>
 						</td>
 					</tr>
@@ -27,9 +25,7 @@
 						<td class="label">Expires in</td>
 						<td>
 							<select name="duration">
-								{{#durations}}
-								<option value="{{value}}">{{value}}</option>
-								{{/durations}}
+								@@durations@@
 							</select>
 						</td>
 					</tr>
@@ -40,6 +36,6 @@
 					</tr>
 				</table>
 
-				<textarea id="code" class="textarea" placeholder="What do you want to share?" rows="10" name="code">{{code}}</textarea>
+				<textarea id="code" class="textarea" placeholder="What do you want to share?" rows="10" name="code">@@code@@</textarea>
 				<input class="submit" type="submit" value="paste" />
 			</form>
--- a/html/paste.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/paste.html	Fri Mar 17 07:43:20 2023 +0100
@@ -1,42 +1,42 @@
-	<h1>Paste {{title}}</h1>
+	<h1>Paste @@title@@</h1>
 
 	<ul id="paste-menu">
-		<li><a href="/fork/{{id}}">Fork</a></li>
-		<li><a href="/download/{{id}}">Download</a></li>
+		<li><a href="/fork/@@id@@">Fork</a></li>
+		<li><a href="/download/@@id@@">Download</a></li>
 	</ul>
 
 	<table>
 		<tbody>
 			<tr>
 				<td class="label">Identifier</td>
-				<td>{{id}}</td>
+				<td>@@id@@</td>
 			</tr>
 			<tr>
 				<td class="label">Title</td>
-				<td>{{title}}</td>
+				<td>@@title@@</td>
 			</tr>
 			<tr>
 				<td class="label">Author</td>
-				<td>{{author}}</td>
+				<td>@@author@@</td>
 			</tr>
 			<tr>
 				<td class="label">Language</td>
-				<td>{{language}}</td>
+				<td>@@language@@</td>
 			</tr>
 			<tr>
 				<td class="label">Date</td>
-				<td>{{date}}</td>
+				<td>@@date@@</td>
 			</tr>
 			<tr>
 				<td class="label">Public</td>
-				<td>{{public}}</td>
+				<td>@@public@@</td>
 			</tr>
 			<tr>
 				<td class="label">Expires in</td>
-				<td>{{expires}}</td>
+				<td>@@expires@@</td>
 			</tr>
 		</tbody>
 	</table>
 	<pre>
-{{code}}
+@@code@@
 	</pre>
--- a/html/search.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/search.html	Fri Mar 17 07:43:20 2023 +0100
@@ -16,9 +16,7 @@
 				<td>Language</td>
 				<td>
 					<select name="language">
-						{{#languages}}
-						<option name="{{name}}">{{name}}</option>
-						{{/languages}}
+						@@languages@@
 					</select>
 				</td>
 			</tr>
--- a/html/status.html	Thu Mar 16 20:45:59 2023 +0100
+++ b/html/status.html	Fri Mar 17 07:43:20 2023 +0100
@@ -1,1 +1,1 @@
-		<h1>{{code}} -- {{status}}</h1>
+		<h1>@@code@@ -- @@message@@</h1>
--- a/http.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/http.c	Fri Mar 17 07:43:20 2023 +0100
@@ -16,17 +16,11 @@
  * 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 "http.h"
 #include "log.h"
-
 #include "page-download.h"
 #include "page-fork.h"
 #include "page-index.h"
@@ -34,6 +28,7 @@
 #include "page-paste.h"
 #include "page-search.h"
 #include "page-static.h"
+#include "page-status.h"
 #include "page.h"
 
 enum page {
--- a/json-util.c	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-/*
- * json-util.c -- utilities for JSON
- *
- * 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 <assert.h>
-#include <string.h>
-
-#include "json-util.h"
-#include "util.h"
-
-json_t *
-ju_languages(const char *selected)
-{
-	json_t *array, *obj;
-
-	array = json_array();
-
-	for (size_t i = 0; i < languagesz; ++i) {
-		if (selected && strcmp(languages[i], selected) == 0)
-			obj = json_pack("{ss ss}",
-				"name",         languages[i],
-				"selected",     "selected"
-			);
-		else
-			obj = json_pack("{ss}", "name", languages[i]);
-
-		json_array_append_new(array, obj);
-	}
-
-	return array;
-}
-
-json_t *
-ju_durations(void)
-{
-	json_t *array = json_array();
-
-	for (size_t i = 0; i < durationsz; ++i)
-		json_array_append_new(array, json_pack("{ss}",
-		    "value", durations[i].title)
-		);
-
-	return array;
-}
-
-json_t *
-ju_date(time_t timestamp)
-{
-	return json_string(bstrftime("%c", localtime(&timestamp)));
-}
-
-json_t *
-ju_expires(time_t timestamp, int duration)
-{
-	return json_string(ttl(timestamp, duration));
-}
-
-const char *
-ju_get_string(const json_t *doc, const char *key)
-{
-	const json_t *val;
-
-	if (!doc || !(val = json_object_get(doc, key)) || !json_is_string(val))
-		return NULL;
-
-	return json_string_value(val);
-}
-
-intmax_t
-ju_get_int(const json_t *doc, const char *key)
-{
-	const json_t *val;
-
-	if (!doc || !(val = json_object_get(doc, key)) || !json_is_integer(val))
-		return 0;
-
-	return json_integer_value(val);
-}
-
-int
-ju_get_bool(const json_t *doc, const char *key)
-{
-	const json_t *val;
-
-	if (!doc || !(val = json_object_get(doc, key)) || !json_is_boolean(val))
-		return 0;
-
-	return json_boolean_value(val);
-}
-
-json_t *
-ju_paste_new(void)
-{
-	return json_pack("{ss ss ss ss si si}",
-		"title",        "Untitled",
-		"author",       "Anonymous",
-		"language",     "nohighlight",
-		"code",         "The best code is no code",
-		"visible",      0,
-		"duration",     PASTE_DURATION_HOUR
-	);
-}
-
-json_t *
-ju_extend(json_t *doc, const char *fmt, ...)
-{
-	assert(fmt);
-
-	json_t *ret, *val;
-	json_error_t err;
-	va_list ap;
-	const char *key;
-
-	va_start(ap, fmt);
-	ret = json_vpack_ex(&err, 0, fmt, ap);
-	va_end(ap);
-
-	/* Now steal every nodes from doc and put them in ret. */
-	if (doc) {
-		json_object_foreach(doc, key, val)
-			json_object_set(ret, key, val);
-
-		json_decref(doc);
-	}
-
-	return ret;
-}
--- a/json-util.h	Thu Mar 16 20:45:59 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-/*
- * json-util.h -- utilities for JSON
- *
- * 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_PASTER_JSON_UTIL_H
-#define PASTER_PASTER_JSON_UTIL_H
-
-#include <stdint.h>
-#include <time.h>
-
-#include <jansson.h>
-
-/**
- * Create an array of all possible languages supported by the application. If
- * the selected argument is not null it will also add a JSON property selected
- * (mostly used when rendering an existing paste).
- *
- * Example of generated schema:
- *
- * ```javascript
- * [
- *   {
- *     "name": "nohighlight"
- *   }
- *   {
- *     "name": "c",
- *     "selected": "selected"
- *   }
- *   {
- *     "name": "cpp"
- *   }
- * ]
- * ```
- *
- * \param selected the current selected language (or NULL if none)
- * \return a JSON array of objects
- */
-json_t *
-ju_languages(const char *selected);
-
-/**
- * Create a list of duration in the form:
- *
- * ```javascript
- * [
- *   {
- *     "value": "day"
- *   }
- *   {
- *     "value": "hour"
- *   }
- * ]
- * ```
- *
- * \return a JSON array of objects
- */
-json_t *
-ju_durations(void);
-
-/**
- * Create a convenient ISO date string containing the paste creation date.
- *
- * \param timestamp the timestamp
- * \return a string with an ISO date
- */
-json_t *
-ju_date(time_t timestamp);
-
-/**
- * Create a convenient remaining time for the given timestamp/duration.
- *
- * Returns strings in the form:
- *
- * - `2 day(s)`
- * - `3 hours(s)`
- *
- * \param timestamp the timestamp
- * \param duration the duration in seconds (e.g. 3600)
- * \return a string containing the expiration time
- */
-json_t *
-ju_expires(time_t timestamp, int duration);
-
-const char *
-ju_get_string(const json_t *, const char *);
-
-intmax_t
-ju_get_int(const json_t *, const char *);
-
-int
-ju_get_bool(const json_t *, const char *);
-
-json_t *
-ju_paste_new(void);
-
-json_t *
-ju_extend(json_t *doc, const char *fmt, ...);
-
-#endif /* !PASTER_PASTER_JSON_UTIL_H */
--- a/log.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/log.c	Fri Mar 17 07:43:20 2023 +0100
@@ -2,11 +2,11 @@
  * log.c -- logging routines
  *
  * 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
@@ -42,7 +42,7 @@
 	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
 	assert(fmt);
 
-	if (config.verbosity >= level) {
+	if (config.verbosity >= (int)level) {
 		va_list ap;
 
 		va_start(ap, fmt);
--- a/page-download.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-download.c	Fri Mar 17 07:43:20 2023 +0100
@@ -19,15 +19,16 @@
 #include <assert.h>
 
 #include "database.h"
+#include "page-status.h"
 #include "page.h"
-#include "json-util.h"
+#include "paste.h"
 
 static void
 get(struct kreq *req)
 {
-	json_t *paste;
+	struct paste paste;
 
-	if (!(paste = database_get(req->path)))
+	if (database_get(&paste, req->path) < 0)
 		page_status(req, KHTTP_404);
 	else {
 		khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_OCTET_STREAM]);
@@ -37,13 +38,12 @@
 #endif
 		khttp_head(req, kresps[KRESP_CONNECTION], "keep-alive");
 		khttp_head(req, kresps[KRESP_CONTENT_DISPOSITION], "attachment; filename=\"%s.%s\"",
-		    ju_get_string(paste, "id"),
-		    ju_get_string(paste, "language")
+			paste.id, paste.language
 		);
 		khttp_body(req);
-		khttp_puts(req, ju_get_string(paste, "code"));
+		khttp_puts(req, paste.code);
 		khttp_free(req);
-		json_decref(paste);
+		paste_finish(&paste);
 	}
 }
 
--- a/page-fork.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-fork.c	Fri Mar 17 07:43:20 2023 +0100
@@ -19,19 +19,22 @@
 #include <assert.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "page-new.h"
+#include "page-status.h"
 #include "page.h"
+#include "paste.h"
 
 static void
 get(struct kreq *req)
 {
-	json_t *paste;
+	struct paste paste;
 
-	if (!(paste = database_get(req->path)))
+	if (database_get(&paste, req->path) < 0)
 		page_status(req, KHTTP_404);
-	else
-		page_new_render(req, paste);
+	else {
+		page_new_render(req, &paste);
+		paste_finish(&paste);
+	}
 }
 
 void
--- a/page-index.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-index.c	Fri Mar 17 07:43:20 2023 +0100
@@ -19,9 +19,10 @@
 #include <assert.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "page-index.h"
+#include "page-status.h"
 #include "page.h"
+#include "paste.h"
 #include "util.h"
 
 #include "html/index.h"
@@ -29,27 +30,112 @@
 #define LIMIT 16
 #define TITLE "paster -- recent pastes"
 
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+	const struct paste *pastes;
+	const size_t pastesz;
+};
+
+enum {
+	KEYWORD_PASTES
+};
+
+static const char * const keywords[] = {
+	[KEYWORD_PASTES] = "pastes"
+};
+
+static int
+format(size_t keyword, void *data)
+{
+	struct page *page = data;
+	struct khtmlreq html;
+	const struct paste *paste;
+
+	khtml_open(&html, page->req, KHTML_PRETTY);
+
+	switch (keyword) {
+	case KEYWORD_PASTES:
+		for (size_t i = 0; i < page->pastesz; ++i) {
+			paste = &page->pastes[i];
+
+			khtml_elem(&html, KELEM_TR);
+
+			/* link */
+			khtml_elem(&html, KELEM_TD);
+			khtml_attr(&html, KELEM_A,
+			    KATTR_HREF, bprintf("/paste/%s", paste->id), KATTR__MAX);
+			khtml_printf(&html, paste->title);
+			khtml_closeelem(&html, 1);
+
+			/* author */
+			khtml_elem(&html, KELEM_TD);
+			khtml_puts(&html, paste->author);
+			khtml_closeelem(&html, 1);
+
+			/* language */
+			khtml_elem(&html, KELEM_TD);
+			khtml_puts(&html, paste->language);
+			khtml_closeelem(&html, 1);
+
+			/* date */
+			khtml_elem(&html, KELEM_TD);
+			khtml_puts(&html, bstrftime("%F %T", localtime(&paste->timestamp)));
+			khtml_closeelem(&html, 1);
+
+			/* expiration */
+			khtml_elem(&html, KELEM_TD);
+			khtml_puts(&html, ttl(paste->timestamp, paste->duration));
+			khtml_closeelem(&html, 1);
+
+			khtml_closeelem(&html, 1);
+
+		}
+		break;
+	default:
+		break;
+	}
+
+	khtml_close(&html);
+
+	return 1;
+}
+
 static void
 get(struct kreq *req)
 {
-	json_t *pastes;
+	struct paste pastes[LIMIT];
+	size_t pastesz = NELEM(pastes);
 
-	if (!(pastes = database_recents(LIMIT)))
+	if (database_recents(pastes, &pastesz) < 0)
 		page_status(req, KHTTP_500);
-	else
-		page_index_render(req, pastes);
+	else {
+		page_index_render(req, pastes, pastesz);
+
+		for (size_t i = 0; i < pastesz; ++i)
+			paste_finish(&pastes[i]);
+	}
 }
 
 void
-page_index_render(struct kreq *req, json_t *pastes)
+page_index_render(struct kreq *req, const struct paste *pastes, size_t pastesz)
 {
 	assert(req);
 	assert(pastes);
 
-	page(req, KHTTP_200, html_index, json_pack("{ss so}",
-		"title",        TITLE,
-		"pastes",       pastes
-	));
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		},
+		.pastes = pastes,
+		.pastesz = pastesz
+	};
+
+	page(req, KHTTP_200, TITLE, html_index, &self.template);
 }
 
 void
--- a/page-index.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-index.h	Fri Mar 17 07:43:20 2023 +0100
@@ -19,12 +19,15 @@
 #ifndef PASTER_PAGE_INDEX_H
 #define PASTER_PAGE_INDEX_H
 
-#include <jansson.h>
+#include <stddef.h>
 
 struct kreq;
+struct paste;
 
 void
-page_index_render(struct kreq *, json_t *pastes);
+page_index_render(struct kreq *req,
+                  const struct paste *pastes,
+                  size_t pastesz);
 
 void
 page_index(struct kreq *);
--- a/page-new.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-new.c	Fri Mar 17 07:43:20 2023 +0100
@@ -18,17 +18,41 @@
 
 #include <assert.h>
 #include <string.h>
+#include <stdlib.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "page-new.h"
+#include "page-status.h"
 #include "page.h"
+#include "paste.h"
 #include "util.h"
 
 #include "html/new.h"
 
 #define TITLE "paster -- create a new paste"
 
+enum {
+	KEYWORD_TITLE,
+	KEYWORD_AUTHOR,
+	KEYWORD_LANGUAGES,
+	KEYWORD_DURATIONS,
+	KEYWORD_CODE
+};
+
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+	const struct paste *paste;
+};
+
+static const char * const keywords[] = {
+	[KEYWORD_TITLE]         = "title",
+	[KEYWORD_AUTHOR]        = "author",
+	[KEYWORD_LANGUAGES]     = "languages",
+	[KEYWORD_DURATIONS]     = "durations",
+	[KEYWORD_CODE]          = "code"
+};
+
 static long long int
 duration(const char *val)
 {
@@ -40,6 +64,57 @@
 	return 60 * 60 * 24;
 }
 
+static int
+format(size_t kw, void *data)
+{
+	struct page *page = data;
+	struct khtmlreq html;
+
+	khtml_open(&html, page->req, KHTML_PRETTY);
+
+	switch (kw) {
+	case KEYWORD_TITLE:
+		if (page->paste)
+			khtml_printf(&html, page->paste->title);
+		break;
+	case KEYWORD_AUTHOR:
+		if (page->paste)
+			khtml_printf(&html, page->paste->author);
+		break;
+	case KEYWORD_LANGUAGES:
+		for (size_t i = 0; i < languagesz; ++i) {
+			if (page->paste && strcmp(page->paste->language, languages[i]) == 0)
+				khtml_attr(&html, KELEM_OPTION,
+				    KATTR_VALUE, languages[i],
+				    KATTR_SELECTED, "selected",
+				    KATTR__MAX
+				);
+			else
+				khtml_attr(&html, KELEM_OPTION, KATTR_VALUE, languages[i], KATTR__MAX);
+			khtml_printf(&html, "%s", languages[i]);
+			khtml_closeelem(&html, 1);
+		}
+		break;
+	case KEYWORD_DURATIONS:
+		for (size_t i = 0; i < durationsz; ++i) {
+			khtml_attr(&html, KELEM_OPTION, KATTR_VALUE, durations[i].title, KATTR__MAX);
+			khtml_printf(&html, "%s", durations[i].title);
+			khtml_closeelem(&html, 1);
+		}
+		break;
+	case KEYWORD_CODE:
+		if (page->paste)
+			khtml_puts(&html, page->paste->code);
+		break;
+	default:
+		break;
+	}
+
+	khtml_close(&html);
+
+	return 1;
+}
+
 static void
 get(struct kreq *r)
 {
@@ -49,68 +124,73 @@
 static void
 post(struct kreq *req)
 {
-	const char *key, *val, *id, *scheme;
-	json_t *paste;
+	struct paste paste;
+	const char *key, *val, *scheme;
 	int raw = 0;
 
-	paste = ju_paste_new();
+	paste_init(&paste);
 
+	// TODO: add verification support.
 	for (size_t i = 0; i < req->fieldsz; ++i) {
 		key = req->fields[i].key;
 		val = req->fields[i].val;
 
 		if (strcmp(key, "title") == 0 && strlen(val))
-			json_object_set_new(paste, "title", json_string(val));
+			replace(&paste.title, val);
 		else if (strcmp(key, "author") == 0 && strlen(val))
-			json_object_set_new(paste, "author", json_string(val));
+			replace(&paste.author, val);
 		else if (strcmp(key, "language") == 0)
-			json_object_set_new(paste, "language", json_string(val));
+			replace(&paste.language, val);
 		else if (strcmp(key, "duration") == 0)
-			json_object_set_new(paste, "duration", json_integer(duration(val)));
+			paste.duration = duration(val);
 		else if (strcmp(key, "code") == 0)
-			json_object_set_new(paste, "code", json_string(val));
+			replace(&paste.code, val);
 		else if (strcmp(key, "visible") == 0)
-			json_object_set_new(paste, "visible", json_boolean(strcmp(val, "on") == 0));
+			paste.visible = strcmp(val, "on") == 0;
 		else if (strcmp(key, "raw") == 0)
 			raw = strcmp(val, "on") == 0;
 	}
 
-	if (database_insert(paste) < 0)
+	if (database_insert(&paste) < 0)
 		page_status(req, KHTTP_500);
 	else {
-		id = ju_get_string(paste, "id");
 		scheme = req->scheme == KSCHEME_HTTP ? "http" : "https";
 
 		if (raw) {
 			/* For CLI users (e.g. paster) just print the location. */
 			khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_201]);
 			khttp_body(req);
-			khttp_printf(req, "%s://%s/paste/%s\n", scheme, req->host, id);
+			khttp_printf(req, "%s://%s/paste/%s\n", scheme, req->host, paste.id);
 		} else {
 			/* Otherwise, redirect to paste details. */
 			khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_302]);
-			khttp_head(req, kresps[KRESP_LOCATION], "/paste/%s", id);
+			khttp_head(req, kresps[KRESP_LOCATION], "/paste/%s", paste.id);
 			khttp_body(req);
 		}
 
 		khttp_free(req);
 	}
 
-	json_decref(paste);
+	paste_finish(&paste);
 }
 
-#include "log.h"
-
 void
-page_new_render(struct kreq *req, json_t *paste)
+page_new_render(struct kreq *req, const struct paste *paste)
 {
 	assert(req);
 
-	page(req, KHTTP_200, html_new, ju_extend(paste, "{ss so so}",
-		"pagetitle", TITLE,
-		"durations", ju_durations(),
-		"languages", ju_languages(ju_get_string(paste, "language"))
-	));
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		},
+		.paste = paste
+	};
+
+	page(req, KHTTP_200, TITLE, html_new, &self.template);
 }
 
 void
--- a/page-new.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-new.h	Fri Mar 17 07:43:20 2023 +0100
@@ -19,12 +19,11 @@
 #ifndef PASTER_PAGE_NEW_H
 #define PASTER_PAGE_NEW_H
 
-#include <jansson.h>
-
 struct kreq;
+struct paste;
 
 void
-page_new_render(struct kreq *, json_t *);
+page_new_render(struct kreq *, const struct paste *);
 
 void
 page_new(struct kreq *);
--- a/page-paste.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-paste.c	Fri Mar 17 07:43:20 2023 +0100
@@ -19,56 +19,104 @@
 #include <assert.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "page-paste.h"
+#include "page-status.h"
 #include "page.h"
+#include "paste.h"
 #include "util.h"
 
 #include "html/paste.h"
 
-static inline json_t *
-mk_pagetitle(const json_t *paste)
-{
-	return json_sprintf("paster -- %s", ju_get_string(paste, "title"));
-}
+#define TITLE "paster -- paste details"
+
+enum {
+	KEYWORD_TITLE,
+	KEYWORD_ID,
+	KEYWORD_AUTHOR,
+	KEYWORD_LANGUAGE,
+	KEYWORD_DATE,
+	KEYWORD_PUBLIC,
+	KEYWORD_EXPIRES,
+	KEYWORD_CODE
+};
 
-static inline json_t *
-mk_date(const json_t *paste)
-{
-	return ju_date(ju_get_int(paste, "timestamp"));
-}
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+	struct paste paste;
+};
+
+static const char * const keywords[] = {
+	[KEYWORD_TITLE]         = "title",
+	[KEYWORD_ID]            = "id",
+	[KEYWORD_AUTHOR]        = "author",
+	[KEYWORD_LANGUAGE]      = "language",
+	[KEYWORD_DATE]          = "date",
+	[KEYWORD_PUBLIC]        = "public",
+	[KEYWORD_EXPIRES]       = "expires",
+	[KEYWORD_CODE]          = "code"
+};
 
-static inline json_t *
-mk_public(const json_t *paste)
+static int
+format(size_t keyword, void *data)
 {
-	const intmax_t visible = ju_get_int(paste, "visible");
+	struct page *page = data;
+	struct khtmlreq html;
 
-	return json_string(visible ? "Yes" : "No");
-}
+	khtml_open(&html, page->req, 0);
 
-static inline json_t *
-mk_expires(const json_t *paste)
-{
-	return ju_expires(
-	    ju_get_int(paste, "timestamp"),
-	    ju_get_int(paste, "duration")
-	);
+	switch (keyword) {
+	case KEYWORD_TITLE:
+		khtml_puts(&html, page->paste.title);
+		break;
+	case KEYWORD_ID:
+		khtml_puts(&html, page->paste.id);
+		break;
+	case KEYWORD_AUTHOR:
+		khtml_puts(&html, page->paste.author);
+		break;
+	case KEYWORD_LANGUAGE:
+		khtml_puts(&html, page->paste.language);
+		break;
+	case KEYWORD_DATE:
+		khtml_puts(&html, bstrftime("%F %T", localtime(&page->paste.timestamp)));
+		break;
+	case KEYWORD_PUBLIC:
+		khtml_puts(&html, page->paste.visible ? "Yes" : "No");
+		break;
+	case KEYWORD_EXPIRES:
+		khtml_puts(&html, ttl(page->paste.timestamp, page->paste.duration));
+		break;
+	case KEYWORD_CODE:
+		khtml_puts(&html, page->paste.code);
+		break;
+	default:
+		break;
+	}
+
+	khtml_close(&html);
+
+	return 1;
 }
 
 static void
 get(struct kreq *req)
 {
-	json_t *paste;
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		}
+	};
 
-	if (!(paste = database_get(req->path)))
+	if (database_get(&self.paste, req->path) < 0)
 		page_status(req, KHTTP_404);
 	else {
-		page(req, KHTTP_200, html_paste, ju_extend(paste, "{so so so so}",
-			"pagetitle",    mk_pagetitle(paste),
-			"date",         mk_date(paste),
-			"public",       mk_public(paste),
-			"expires",      mk_expires(paste)
-		));
+		page(req, KHTTP_200, TITLE, html_paste, &self.template);
+		paste_finish(&self.paste);
 	}
 }
 
--- a/page-paste.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-paste.h	Fri Mar 17 07:43:20 2023 +0100
@@ -2,11 +2,11 @@
  * page-paste.h -- page /paste/<id>
  *
  * 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
@@ -16,12 +16,12 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef PASTER_PAGE_IMAGE_H
-#define PASTER_PAGE_IMAGE_H
+#ifndef PASTER_PAGE_PASTE_H
+#define PASTER_PAGE_PASTE_H
 
 struct kreq;
 
 void
 page_paste(struct kreq *);
 
-#endif /* !PASTER_PAGE_IMAGE_H */
+#endif /* !PASTER_PAGE_PASTE_H */
--- a/page-search.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page-search.c	Fri Mar 17 07:43:20 2023 +0100
@@ -20,10 +20,11 @@
 #include <string.h>
 
 #include "database.h"
-#include "json-util.h"
 #include "page-index.h"
 #include "page-search.h"
+#include "page-status.h"
 #include "page.h"
+#include "paste.h"
 #include "util.h"
 
 #include "html/search.h"
@@ -31,19 +32,65 @@
 #define TITLE    "paster -- search"
 #define LIMIT    16
 
+enum {
+	KEYWORD_LANGUAGES
+};
+
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+};
+
+static const char * const keywords[] = {
+	[KEYWORD_LANGUAGES] = "languages"
+};
+
+static int
+format(size_t keyword, void *data)
+{
+	struct page *page = data;
+	struct khtmlreq html;
+
+	khtml_open(&html, page->req, 0);
+
+	switch (keyword) {
+	case KEYWORD_LANGUAGES:
+		for (size_t i = 0; i < languagesz; ++i) {
+			khtml_attr(&html, KELEM_OPTION, KATTR_NAME, languages[i], KATTR__MAX);
+			khtml_puts(&html, languages[i]);
+			khtml_closeelem(&html, 1);
+		}
+		break;
+	default:
+		break;
+	}
+
+	khtml_close(&html);
+
+	return 1;
+}
+
 static void
 get(struct kreq *req)
 {
-	page(req, KHTTP_200, html_search, json_pack("{ss so}",
-		"pagetitle",    "paster -- search",
-		"languages",    ju_languages(NULL)
-	));
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		}
+	};
+
+	page(req, KHTTP_200, TITLE, html_search, &self.template);
 }
 
 static void
 post(struct kreq *req)
 {
-	json_t *pastes;
+	struct paste pastes[LIMIT];
+	size_t pastesz = NELEM(pastes);
 	const char *key, *val, *title = NULL, *author = NULL, *language = NULL;
 
 	for (size_t i = 0; i < req->fieldsz; ++i) {
@@ -64,10 +111,14 @@
 	if (author && strlen(author) == 0)
 		author = NULL;
 
-	if (!(pastes = database_search(16, title, author, language)))
+	if (database_search(pastes, &pastesz, title, author, language) < 0)
 		page_status(req, KHTTP_500);
-	else
-		page_index_render(req, pastes);
+	else {
+		page_index_render(req, pastes, pastesz);
+
+		for (size_t i = 0; i < pastesz; ++i)
+			paste_finish(&pastes[i]);
+	}
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-status.c	Fri Mar 17 07:43:20 2023 +0100
@@ -0,0 +1,92 @@
+/*
+ * page-status.c -- error page
+ *
+ * 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 <assert.h>
+
+#include "page.h"
+#include "util.h"
+
+#include "html/status.h"
+
+enum {
+	KEYWORD_CODE,
+	KEYWORD_MESSAGE
+};
+
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+	enum khttp status;
+};
+
+static const int status_codes[] = {
+	[KHTTP_200]             = 200,
+	[KHTTP_400]             = 400,
+	[KHTTP_404]             = 404,
+	[KHTTP_500]             = 500
+};
+
+static const char * const status_messages[] = {
+	[KHTTP_200]             = "OK",
+	[KHTTP_400]             = "Bad Request",
+	[KHTTP_404]             = "Not Found",
+	[KHTTP_500]             = "Internal Server Error"
+};
+
+static const char *keywords[] = {
+	[KEYWORD_CODE]          = "code",
+	[KEYWORD_MESSAGE]       = "message"
+};
+
+static int
+format(size_t keyword, void *data)
+{
+	struct page *page = data;
+
+	switch (keyword) {
+	case KEYWORD_CODE:
+		khttp_printf(page->req, "%d", status_codes[page->status]);
+		break;
+	case KEYWORD_MESSAGE:
+		khttp_printf(page->req, "%s", status_messages[page->status]);
+		break;
+	default:
+		break;
+	}
+
+	return 1;
+}
+
+void
+page_status(struct kreq *req, enum khttp status)
+{
+	assert(req);
+
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		},
+		.status = status
+	};
+
+	page(req, status, "paster -- error", html_status, &self.template);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-status.h	Fri Mar 17 07:43:20 2023 +0100
@@ -0,0 +1,29 @@
+/*
+ * page-status.h -- error page
+ *
+ * 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_PAGE_STATUS_H
+#define PASTER_PAGE_STATUS_H
+
+struct kreq;
+
+enum khttp;
+
+void
+page_status(struct kreq *req, enum khttp status);
+
+#endif /* !PASTER_PAGE_NEW_H */
--- a/page.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/page.c	Fri Mar 17 07:43:20 2023 +0100
@@ -19,8 +19,6 @@
 #include <assert.h>
 #include <string.h>
 
-#include <mustach-jansson.h>
-
 #include "config.h"
 #include "page.h"
 #include "util.h"
@@ -29,64 +27,66 @@
 #include "html/header.h"
 #include "html/status.h"
 
-static const int statustab[] = {
-	[KHTTP_200] = 200,
-	[KHTTP_400] = 400,
-	[KHTTP_404] = 404,
-	[KHTTP_500] = 500
+#define CHAR(html) (const char *)(html)
+
+enum {
+	KEYWORD_TITLE,
 };
 
-static const char * const statusmsg[] = {
-	[KHTTP_200] = "OK",
-	[KHTTP_400] = "Bad Request",
-	[KHTTP_404] = "Not Found",
-	[KHTTP_500] = "Internal Server Error"
+struct page {
+	struct kreq *req;
+	struct ktemplate template;
+	const char *title;
+};
+
+static const char *keywords[] = {
+	[KEYWORD_TITLE] = "title"
 };
 
 static int
-writer(void *data, const char *buffer, size_t size)
+format(size_t keyword, void *data)
 {
-	struct kreq *req = data;
-
-	khttp_write(req, buffer, size);
+	struct page *page = data;
 
-	return MUSTACH_OK;
-}
+	switch (keyword) {
+	case KEYWORD_TITLE:
+		khttp_printf(page->req, "%s", page->title);
+		break;
+	default:
+		break;
+	}
 
-static void
-format(struct kreq *req, const char *html, json_t *doc)
-{
-	if (!doc)
-		khttp_template_buf(req, NULL, html, strlen(html));
-	else
-		mustach_jansson_write(html, strlen(html), doc, 0, writer, req);
+	return 1;
 }
 
 void
-page(struct kreq *req, enum khttp status, const unsigned char *html, json_t *doc)
+page(struct kreq *req,
+     enum khttp status,
+     const char *title,
+     const unsigned char *html,
+     const struct ktemplate *tmpl)
 {
 	assert(req);
+	assert(title);
 	assert(html);
+	assert(tmpl);
+
+	struct page self = {
+		.req = req,
+		.template = {
+			.cb = format,
+			.arg = &self,
+			.key = keywords,
+			.keysz = NELEM(keywords)
+		},
+		.title = title,
+	};
 
 	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML]);
 	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
 	khttp_body(req);
-	format(req, (const char *)html_header, doc);
-	format(req, (const char *)html, doc);
-	format(req, (const char *)html_footer, doc);
+	khttp_template_buf(req, &self.template, CHAR(html_header), strlen(CHAR(html_header)));
+	khttp_template_buf(req, tmpl, CHAR(html), strlen(CHAR(html)));
+	khttp_template_buf(req, NULL, CHAR(html_footer), strlen(CHAR(html_footer)));
 	khttp_free(req);
-
-	if (doc)
-		json_decref(doc);
 }
-
-void
-page_status(struct kreq *req, enum khttp status)
-{
-	assert(req);
-
-	page(req, status, html_status, json_pack("{si ss}",
-		"code",         statustab[status],
-		"status",       statusmsg[status]
-	));
-}
--- a/page.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/page.h	Fri Mar 17 07:43:20 2023 +0100
@@ -2,11 +2,11 @@
  * page.h -- page renderer
  *
  * 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
@@ -23,14 +23,14 @@
 #include <stdarg.h>
 #include <stdint.h>
 
-#include <jansson.h>
-
 #include <kcgi.h>
+#include <kcgihtml.h>
 
 void
-page(struct kreq *req, enum khttp status, const unsigned char *html, json_t *doc);
-
-void
-page_status(struct kreq *req, enum khttp status);
+page(struct kreq *req,
+     enum khttp status,
+     const char *title,
+     const unsigned char *html,
+     const struct ktemplate *tmpl);
 
 #endif /* !PASTER_PAGE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/paste.c	Fri Mar 17 07:43:20 2023 +0100
@@ -0,0 +1,50 @@
+/*
+ * paste.c -- paste definition
+ *
+ * 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 <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "paste.h"
+#include "util.h"
+
+void
+paste_init(struct paste *paste)
+{
+	assert(paste);
+
+	memset(paste, 0, sizeof (*paste));
+	paste->title = estrdup(PASTE_DEFAULT_TITLE);
+	paste->author = estrdup(PASTE_DEFAULT_AUTHOR);
+	paste->language = estrdup(PASTE_DEFAULT_LANGUAGE);
+	paste->timestamp = time(NULL);
+	paste->duration = PASTE_DURATION_DAY;
+}
+
+void
+paste_finish(struct paste *paste)
+{
+	assert(paste);
+
+	free(paste->id);
+	free(paste->title);
+	free(paste->author);
+	free(paste->language);
+	free(paste->code);
+	memset(paste, 0, sizeof (struct paste));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/paste.h	Fri Mar 17 07:43:20 2023 +0100
@@ -0,0 +1,50 @@
+/*
+ * paste.h -- paste definition
+ *
+ * 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_PASTE_H
+#define PASTER_PASTE_H
+
+#include <time.h>
+
+#define PASTE_DURATION_HOUR      3600           /* Seconds in one hour. */
+#define PASTE_DURATION_DAY       86400          /* Seconds in one day. */
+#define PASTE_DURATION_WEEK      604800         /* Seconds in one week. */
+#define PASTE_DURATION_MONTH     2592000        /* Rounded to 30 days. */
+
+#define PASTE_DEFAULT_TITLE      "Untitled"
+#define PASTE_DEFAULT_AUTHOR     "Anonymous"
+#define PASTE_DEFAULT_LANGUAGE   "nohighlight"
+
+struct paste {
+	char *id;
+	char *title;
+	char *author;
+	char *language;
+	char *code;
+	time_t timestamp;
+	int visible;
+	int duration;
+};
+
+void
+paste_init(struct paste *paste);
+
+void
+paste_finish(struct paste *paste);
+
+#endif /* !PASTER_PASTE_H */
--- a/pasterd.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/pasterd.c	Fri Mar 17 07:43:20 2023 +0100
@@ -67,7 +67,7 @@
 }
 
 int
-main(int argc, char **argv, char **env)
+main(int argc, char **argv)
 {
 	const char *value;
 	int opt;
--- a/util.c	Thu Mar 16 20:45:59 2023 +0100
+++ b/util.c	Fri Mar 17 07:43:20 2023 +0100
@@ -27,6 +27,7 @@
 #include <time.h>
 
 #include "config.h"
+#include "paste.h"
 #include "util.h"
 
 const char * const languages[] = {
--- a/util.h	Thu Mar 16 20:45:59 2023 +0100
+++ b/util.h	Fri Mar 17 07:43:20 2023 +0100
@@ -24,11 +24,6 @@
 
 #define NELEM(x) (sizeof (x) / sizeof (x)[0])
 
-#define PASTE_DURATION_HOUR     (3600)
-#define PASTE_DURATION_DAY      (PASTE_DURATION_HOUR * 24)
-#define PASTE_DURATION_WEEK     (PASTE_DURATION_DAY * 7)
-#define PASTE_DURATION_MONTH    (PASTE_DURATION_DAY * 30)
-
 struct tm;
 struct kreq;