view scid/theme.c @ 55:38901547a76c

misc: improve error message
author David Demelier <markand@malikania.fr>
date Wed, 17 Aug 2022 11:13:36 +0200
parents 16f1c72d1635
children 070d8c92ca30
line wrap: on
line source

/*
 * theme.c -- theme management
 *
 * Copyright (c) 2021-2022 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 <errno.h>
#include <limits.h>
#include <string.h>

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

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

#define SIGNATURE DUK_HIDDEN_SYMBOL("File")

static duk_context *context;
static char base[PATH_MAX];

/* {{{ 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(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 inline void push_object(duk_context *, json_t *);
static inline void push_array(duk_context *, json_t *);

static inline void
push_value(duk_context *ctx, json_t *val)
{
	switch (json_typeof(val)) {
	case JSON_STRING:
		duk_push_string(ctx, json_string_value(val));
		break;
	case JSON_INTEGER:
		duk_push_number(ctx, json_integer_value(val));
		break;
	case JSON_REAL:
		duk_push_number(ctx, json_real_value(val));
		break;
	case JSON_TRUE:
		duk_push_true(ctx);
		break;
	case JSON_FALSE:
		duk_push_false(ctx);
		break;
	case JSON_OBJECT:
		push_object(ctx, val);
		break;
	case JSON_ARRAY:
		push_array(ctx, val);
		break;
	default:
		duk_push_null(ctx);
		break;
	}
}

static inline void
push_object(duk_context *ctx, json_t *object)
{
	assert(json_is_object(object));

	json_t *val;
	const char *key;

	duk_push_object(ctx);

	json_object_foreach(object, key, val) {
		push_value(ctx, val);
		duk_put_prop_string(ctx, -2, key);
	}
}

static inline void
push_array(duk_context *ctx, json_t *array)
{
	assert(json_is_array(array));

	json_t *val;
	size_t i;

	duk_push_array(ctx);

	json_array_foreach(array, i, val) {
		push_value(ctx, val);
		duk_put_prop_index(ctx, -2, i);
	}
}

static char *
call(json_t *json, const char *function)
{
	char *out = NULL;
	size_t outsz = 0;
	FILE *fp;

	duk_get_global_string(context, function);

	if (duk_is_callable(context, -1)) {
		fp = util_open_memstream(&out, &outsz);
		duk_push_pointer(context, fp);
		push_value(context, json);

		if (duk_pcall(context, 2) != 0)
			log_warn("theme: %s", duk_safe_to_string(context, -1));

		duk_pop(context);
		fclose(fp);
	} else
		duk_pop(context);

	if (!out)
		out = util_strdup("");
	if (json)
		json_decref(json);

	return out;
}

void
theme_open(const char *directory)
{
	assert(directory);

	const char *path;
	char *data;

	util_strlcpy(base, directory, sizeof (base));
	context = duk_create_heap_default();
	path = theme_path("theme.js");

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

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

const char *
theme_path(const char *filename)
{
	assert(filename);

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

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

	return path;
}

char *
theme_render(const char *function, json_t *json)
{
	assert(function);

	return call(json, function);
}

char *
theme_status(int status, const char *message)
{
	return theme_render("onPageStatus", util_json_pack("{si ss}",
		"status",  status,
		"message", message
	));
}

void
theme_finish(void)
{
	duk_destroy_heap(context);
}