view scid/theme.c @ 29:695637f1d8a7

scid: first index page in javascript
author David Demelier <markand@malikania.fr>
date Thu, 04 Aug 2022 14:13:58 +0200
parents 4c16bb25e4f1
children 43333d18e4b8
line wrap: on
line source

#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <string.h>

#include <duktape.h>
#include <jansson.h>
#include <mustache.h>

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

#define SIGNATURE DUK_HIDDEN_SYMBOL("File")

struct theme {
	char base[PATH_MAX];
	duk_context *ctx;
};

/* {{{ mustache support */

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

	const char *path = data;

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

static int
mch_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
mch_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 (mch_out_verbatim("&quot;", 6, fp) < 0)
				return -1;
			break;
		case '&':
			if (mch_out_verbatim("&amp;", 5, fp) < 0)
				return -1;
			break;
		case '<':
			if (mch_out_verbatim("&lt;", 4, fp) < 0)
				return -1;
			break;
		case '>':
			if (mch_out_verbatim("&gt;", 4, fp) < 0)
				return -1;
			break;
		default:
			if (mch_out_verbatim(&output[i], 1, fp) <0)
				return -1;
			break;
		}
	}

	return 0;
}

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

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

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

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

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

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

static int
mch_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 mch_out_int(fp, json_integer_value(value));
	case JSON_REAL:
		return mch_out_double(fp, json_real_value(value));
	case JSON_TRUE:
		return mch_out_verbatim("true", 4, fp);
	case JSON_FALSE:
		return mch_out_verbatim("false", 5, fp);
	default:
		break;
	}

	return 0;
}

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

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

	json_t *value = node;

#if 0
	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 *
mch_get_child_by_index(void *node, unsigned int index, void *pdata)
{
	(void)pdata;

	json_t *value = node;

#if 0
	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 *
mch_get_partial(const char *name, size_t size, void *pdata)
{
	(void)name;
	(void)size;
	(void)pdata;

	return NULL;
}

static int
mch_process(const char *path, const char *input, FILE *fp, json_t *doc)
{
	MUSTACHE_PARSER parser = {
		.parse_error = mch_parse_error
	};
	MUSTACHE_RENDERER rdr = {
		.out_verbatim = mch_out_verbatim,
		.out_escaped = mch_out_escaped
	};
	MUSTACHE_DATAPROVIDER pv = {
		.dump = mch_dump,
		.get_root = mch_get_root,
		.get_child_by_name = mch_get_child_by_name,
		.get_child_by_index = mch_get_child_by_index,
		.get_partial = mch_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;
}

void
mch_templatize(FILE *fp, const char *path, json_t *doc)
{
	assert(path);

	char *in = NULL;

	if ((in = util_read(path))) {
		/* Process template but don't return a partially written file. */
		if (mch_process(path, in, fp, doc) < 0)
			log_warn("theme: mustache error %s", path);

		free(in);
	} else
		log_warn("theme: %s: %s", path, strerror(errno));
}

/* }}} */

/* {{{ Scid module */

static duk_ret_t
Scid_render(duk_context *ctx)
{
	FILE *fp = duk_require_pointer(ctx, 0);
	const char *path = duk_require_string(ctx, 1);
	const char *json = duk_json_encode(ctx, 2);
	json_t *doc = NULL;
	json_error_t err;

	if (json && !(doc = json_loads(json, 0, &err)))
		return duk_error(ctx, DUK_ERR_ERROR, "%d:%d:%s", err.line, err.column, err.text);

	mch_templatize(fp, theme_path(scid.theme, path), doc);

	if (doc)
		json_decref(doc);

	return 0;
}

static duk_ret_t
Scid_print(duk_context *ctx)
{
	puts(duk_require_string(ctx, 0));

	return 0;
}

static const duk_function_list_entry functions[] = {
	{ "render",     Scid_render,    3 },
	{ "print",      Scid_print,     1 },
	{ NULL,         NULL,           0 }
};

/* }}} */

static char *
call(struct theme *t, json_t *json, const char *function)
{
	char *out = NULL, *dump = NULL;
	size_t outsz = 0;
	FILE *fp = NULL;
	int nargs = 1;

	duk_get_global_string(t->ctx, function);

	if (!duk_is_callable(t->ctx, -1))
		goto over;
	if (!(fp = open_memstream(&out, &outsz)))
		goto over;

	duk_push_pointer(t->ctx, fp);

	if (json && (dump = json_dumps(json, JSON_COMPACT))) {
		duk_push_string(t->ctx, dump);
		duk_json_decode(t->ctx, -1);
		nargs++;
	}

	if (duk_pcall(t->ctx, nargs) != 0)
		log_warn("theme: %s", duk_safe_to_string(t->ctx, -1));

over:
	duk_pop(t->ctx);

	/*
	 * For convenience, otherwise all callers have to check for non-NULL
	 * after calling the function.
	 */
	free(dump);

	if (fp)
		fclose(fp);
	if (!out)
		out = util_strdup("");

	return out;
}

const char *
theme_path(struct theme *t, const char *filename)
{
	assert(filename);

	/* Build path to the template file. */
	static _Thread_local char path[PATH_MAX];

	snprintf(path, sizeof (path), "%s/%s", t->base, filename);

	return path;
}

struct theme *
theme_open(const char *directory)
{
	assert(directory);

	struct theme *t;
	char themefile[PATH_MAX], *data;

	t = util_calloc(1, sizeof (*t));
	t->ctx = duk_create_heap_default();
	util_strlcpy(t->base, directory, sizeof (t->base));

	/* Open theme.js in the directory. */
	snprintf(themefile, sizeof (themefile), "%s/theme.js", t->base);

	if (!(data = util_read(themefile)))
		log_warn("theme: %s: %s", themefile, strerror(errno));
	else {
		if (duk_peval_string(t->ctx, data) != 0)
			log_warn("theme: %s", duk_safe_to_string(t->ctx, -1));

		duk_pop(t->ctx);
		duk_push_object(t->ctx);
		duk_put_function_list(t->ctx, -1, functions);
		duk_put_global_string(t->ctx, "Scid");
		free(data);
	}

	return t;
}

char *
theme_page_index(struct theme *t, json_t *json)
{
	assert(t);

	return call(t, json, "onPageIndex");
}

char *
theme_page_status(struct theme *t, enum khttp status)
{
	assert(t);

	(void)t;
	(void)status;

#if 0
	return call(t, json, "onPageStatus");
#endif
	return "ERROR";
}

void
theme_free(struct theme *t)
{
	assert(t);

	duk_destroy_heap(t->ctx);
}