view scid/page.c @ 26:7e10cace67a3

scid: add basic mustache support
author David Demelier <markand@malikania.fr>
date Tue, 02 Aug 2022 13:24:13 +0200
parents 600204c31bf0
children
line wrap: on
line source

/*
 * page.c -- page renderer
 *
 * Copyright (c) 2021 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/stat.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <mustache.h>

#include "log.h"
#include "page.h"
#include "scid.h"
#include "util.h"

static void
parse_error(int code, const char *msg, unsigned int line, unsigned int col, void *data)
{
	(void)code;

	const char *path = data;

	log_warn("page: %s:%u:%u:%s", path, line, col, msg);
}

static int
out_verbatim(const char *output, size_t size, void *data)
{
	FILE *fp = data;

	if (fwrite(output, 1, size, fp) != size)
		return -1;

	return 0;
}

static int
out_escaped(const char *output, size_t size, void *data)
{
	FILE *fp = data;

	for (size_t i = 0; i < size; ++i) {
		switch (output[i]) {
		case '"':
			if (out_verbatim("&quot;", 6, fp) < 0)
				return -1;
			break;
		case '&':
			if (out_verbatim("&amp;", 5, fp) < 0)
				return -1;
			break;
		case '<':
			if (out_verbatim("&lt;", 4, fp) < 0)
				return -1;
			break;
		case '>':
			if (out_verbatim("&gt;", 4, fp) < 0)
				return -1;
			break;
		default:
			if (out_verbatim(&output[i], 1, fp) <0)
				return -1;
			break;
		}
	}

	return 0;
}

static inline int
out_int(FILE *fp, intmax_t val)
{
	char buf[64] = {0};

	snprintf(buf, sizeof (buf), "%jd", val);

	return out_verbatim(buf, strlen(buf), fp);
}

static inline int
out_double(FILE *fp, double val)
{
	char buf[64] = {0};

	snprintf(buf, sizeof (buf), "%f", val);

	return out_verbatim(buf, strlen(buf), fp);
}

static int
dump(void *node, int (*outputter)(const char *, size_t, void *), void *rdata, void *pdata)
{
	(void)pdata;

	FILE *fp = rdata;
	const json_t *value = node;

	switch (json_typeof(value)) {
	case JSON_OBJECT:
	case JSON_ARRAY:
		/* This indicates a document construction error. */
		// TODO: change to error log.
		abort();
		break;
	case JSON_STRING:
		return outputter(json_string_value(value), json_string_length(value), fp);
	case JSON_INTEGER:
		return out_int(fp, json_integer_value(value));
	case JSON_REAL:
		return out_double(fp, json_real_value(value));
	case JSON_TRUE:
		return out_verbatim("true", 4, fp);
	case JSON_FALSE:
		return out_verbatim("false", 5, fp);
	default:
		break;
	}

	return 0;
}

static void *
get_root(void *pdata)
{
	return pdata;
}

static void *
get_child_by_name(void *node, const char *name, size_t size, void *pdata)
{
	(void)pdata;

	json_t *value = node;

#if 1
	printf("-> Seeking name '%.*s'\n", (int)size, name);
	printf("-> Type of node: %d (%p)\n", json_typeof(value), value);
#endif

	if (json_is_object(value))
		return json_object_getn(node, name, size);

	return NULL;
}

static void *
get_child_by_index(void *node, unsigned int index, void *pdata)
{
	(void)pdata;

	json_t *value = node;

#if 1
	printf("-> Seeking index '%u'\n", index);
	printf("-> Type of node: %d (%p)\n", json_typeof(value), value);
#endif

	if (json_is_array(value))
		return json_array_get(node, index);

	return NULL;
}

static MUSTACHE_TEMPLATE *
get_partial(const char *name, size_t size, void *pdata)
{
	(void)name;
	(void)size;
	(void)pdata;

	return NULL;
}

static char *
readall(const char *file)
{
	int fd;
	char *ret = NULL;
	struct stat st;

	if ((fd = open(file, O_RDONLY)) < 0)
		return NULL;
	if (fstat(fd, &st) < 0)
		goto exit;
	if (!(ret = calloc(1, st.st_size + 1)))
		goto exit;
	if (read(fd, ret, st.st_size) != st.st_size) {
		free(ret);
		ret = NULL;
	}

exit:
	close(fd);

	return ret;
}

static int
process(const char *path, const char *input, FILE *fp, json_t *doc)
{
	MUSTACHE_PARSER parser = {
		.parse_error = parse_error
	};
	MUSTACHE_RENDERER rdr = {
		.out_verbatim = out_verbatim,
		.out_escaped = out_escaped
	};
	MUSTACHE_DATAPROVIDER pv = {
		.dump = dump,
		.get_root = get_root,
		.get_child_by_name = get_child_by_name,
		.get_child_by_index = get_child_by_index,
		.get_partial = get_partial
	};
	MUSTACHE_TEMPLATE *tmpl;
	int status;
	
	if (!(tmpl = mustache_compile(input, strlen(input), &parser, (void *)path, 0)))
		return -1;

	status = mustache_process(tmpl, &rdr, fp, &pv, doc);
	mustache_release(tmpl);

	return status ? -1 : 0;
}

char *
templatize(const char *path, json_t *doc)
{
	assert(path);

	char *in = NULL, *out = NULL;
	size_t outsz = 0;
	FILE *fp;

	if (!(fp = open_memstream(&out, &outsz)))
		goto exit;
	if (!(in = readall(path)))
		goto exit;

	/* Process template but don't return a partially written file. */
	if (process(path, in, fp, doc) < 0) {
		fclose(fp);
		free(out);
		fp = NULL;
		out = NULL;
		errno = EINVAL;
	}

exit:
	if (!out)
		log_warn("page: %s: %s", path, strerror(errno));
	if (fp)
		fclose(fp);

	free(in);
	json_decref(doc);

	return out;
}

static void
render(struct kreq *req, const char *path, json_t *doc)
{
	char *content;

	if (!(content = templatize(path, doc)))
		log_warn("page: unable to templatize: %s", strerror(errno));
	if (doc)
		json_decref(doc);

	khttp_printf(req, "%s", content);

	free(content);
}

void
page(struct kreq *req,
     enum khttp status,
     enum kmime mime,
     const char *path,
     json_t *doc)
{
	assert(req);
	assert(!path || (path && doc));

	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
	khttp_body(req);

	if (path) {
		printf("document is: [%s]\n", json_dumps(doc, JSON_INDENT(2)));
		/* TODO: add title in the header. */
		render(req, scid_theme_path("fragments/header.html"), NULL);
		render(req, scid_theme_path(path), doc);
		render(req, scid_theme_path("fragments/footer.html"), NULL);
	}

	khttp_free(req);
}