changeset 1157:3c363a43965a

Merge with v4.5
author Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
date Sun, 30 Dec 2018 13:23:15 -0500
parents c6906a7dbd8e (current diff) 9a70df4e78e9 (diff)
children 68a04057057e
files .gitmodules CMakeLists.txt ast.c daemon.c error.c iter.h parse.h pipeline.c post.c post_fmt3_cmds.c post_fmt4_ast.c ptree.c tag.c template.y utils.h
diffstat 46 files changed, 569 insertions(+), 634 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgsigs	Sun Dec 30 13:23:15 2018 -0500
@@ -0,0 +1,5 @@
+9f2eb71d116d7b3accc7c87fea9b085b15aadde6 0 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
--- a/.hgtags	Sun Aug 06 15:48:33 2017 +0300
+++ b/.hgtags	Sun Dec 30 13:23:15 2018 -0500
@@ -8,3 +8,8 @@
 01e4e479754cd110c4944b374e45dab534678fc5 v4.2
 fe6c3446b55db23a7343bc0ae5b7eeb4601fb24c v4.3
 bd519cc8058c6c15ce882f5c265d55b474638964 v4.4-rc1
+9f2eb71d116d7b3accc7c87fea9b085b15aadde6 v4.4-rc2
+d7ff619768d39bac25ff110ecf6224c2cdfd422a v4.4
+1dc65e2b39e75af9fc4f7a9c1e3517cff4666eea v4.5-rc1
+40e8a3bb0684652444e3090343af2a31ad98da63 v4.5-rc2
+6435f2f8f9181c677db9fdfd42e60937b6966772 v4.5
--- a/CMakeLists.txt	Sun Aug 06 15:48:33 2017 +0300
+++ b/CMakeLists.txt	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+# Copyright (c) 2011-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -118,10 +118,8 @@
 
 target_link_libraries(blahg
 	md
-	avl
 	m
 	${JEFFPC_LIBRARY}
-	${JEFFPC_COMM_LIBRARY}
 )
 
 add_executable(blahgd
--- a/README	Sun Aug 06 15:48:33 2017 +0300
+++ b/README	Sun Dec 30 13:23:15 2018 -0500
@@ -50,7 +50,6 @@
 provided by illumos-based distros (e.g., OmniOS or OpenIndiana hipster).
 Specifically, blahgd requires:
 
-  - libavl.so (for AVL trees)
   - libmd.so (for SHA1)
 
 Other dependencies include:
--- a/ast.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/ast.c	Sun Dec 30 13:23:15 2018 -0500
@@ -29,7 +29,6 @@
 
 #include "ast.h"
 #include "utils.h"
-#include "iter.h"
 
 static struct mem_cache *ast_cache;
 static struct mem_cache *astnode_cache;
@@ -109,7 +108,7 @@
 		struct val *parts[6];
 		int nparts = 0; /* pacify gcc */
 
-		parts[0] = VAL_ALLOC_SYM_CSTR(ast_typename(cur->type));
+		parts[0] = VAL_ALLOC_STR_STATIC(ast_typename(cur->type));
 
 		switch (cur->type) {
 			case AST_ENCAP:
@@ -119,11 +118,11 @@
 			case AST_STR:
 			case AST_VERB:
 			case AST_LIST:
-				parts[1] = VAL_ALLOC_STR(cur->u.str);
+				parts[1] = str_getref_val(cur->u.str);
 				nparts = 2;
 				break;
 			case AST_CMD:
-				parts[1] = VAL_DUP_CSTR(cur->u.cmd.info->name);
+				parts[1] = VAL_DUP_STR(cur->u.cmd.info->name);
 				parts[2] = VAL_ALLOC_BOOL(cur->u.cmd.bad);
 				parts[3] = VAL_ALLOC_INT(cur->u.cmd.capture_nmand);
 				parts[4] = VAL_ALLOC_INT(cur->u.cmd.capture_nopt);
@@ -141,7 +140,7 @@
 				break;
 			case AST_ENV:
 				if (cur->u.env.name)
-					parts[1] = VAL_ALLOC_STR(cur->u.env.name);
+					parts[1] = str_getref_val(cur->u.env.name);
 				else
 					parts[1] = VAL_ALLOC_CONS(NULL, NULL);
 				parts[2] = __ast_dump_to_lisp(&cur->u.env.children);
@@ -157,7 +156,7 @@
 			case AST_CHAR:
 				char2str[0] = cur->u.ch.ch;
 				char2str[1] = '\0';
-				parts[1] = VAL_DUP_CSTR(char2str);
+				parts[1] = VAL_DUP_STR(char2str);
 				parts[2] = VAL_ALLOC_INT(cur->u.ch.len);
 				nparts = 3;
 				break;
--- a/cmake/Modules/Findjeffpc.cmake	Sun Aug 06 15:48:33 2017 +0300
+++ b/cmake/Modules/Findjeffpc.cmake	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2016-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+# Copyright (c) 2016-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -26,12 +26,10 @@
 # This module defines:
 #   JEFFPC_INCLUDE_DIR
 #   JEFFPC_LIBRARY
-#   JEFFPC_COMM_LIBRARY
 #   JEFFPC_FOUND
 #
 # Use the following variables to help locate the library and header files.
 #   WITH_JEFFPC_LIB=x      - directory containing libjeffpc.so
-#   WITH_JEFFPC_COMM_LIB=x - directory containing libjeffpc-comm.so
 #   WITH_JEFFPC_INCLUDES=x - directory containing jeffpc/jeffpc.h
 #   WITH_JEFFPC=x          - same as setting WITH_JEFFPC_LIB=x/lib and
 #                            WITH_JEFFPC_INCLUDES=x/include
@@ -41,9 +39,6 @@
 	if (NOT WITH_JEFFPC_LIB)
 		set(WITH_JEFFPC_LIB "${WITH_JEFFPC}/lib")
 	endif()
-	if (NOT WITH_JEFFPC_COMM_LIB)
-		set(WITH_JEFFPC_COMM_LIB "${WITH_JEFFPC}/lib")
-	endif()
 	if (NOT WITH_JEFFPC_INCLUDES)
 		set(WITH_JEFFPC_INCLUDES "${WITH_JEFFPC}/include")
 	endif()
@@ -54,9 +49,6 @@
 find_library(JEFFPC_LIBRARY NAMES jeffpc
 	PATHS ${WITH_JEFFPC_LIB}
 )
-find_library(JEFFPC_COMM_LIBRARY NAMES jeffpc-comm
-	PATHS ${WITH_JEFFPC_COMM_LIB}
-)
 
 #
 # Handle the QUIETLY and REQUIRED arguments and set JEFFPC_FOUND to TRUE if
@@ -65,5 +57,3 @@
 include(FindPackageHandleStandardArgs)
 find_package_handle_standard_args(JEFFPC DEFAULT_MSG JEFFPC_LIBRARY
 	JEFFPC_INCLUDE_DIR)
-find_package_handle_standard_args(JEFFPC_COMM DEFAULT_MSG JEFFPC_COMM_LIBRARY
-	JEFFPC_INCLUDE_DIR)
--- a/comment.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/comment.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -34,7 +34,6 @@
 #include <jeffpc/error.h>
 #include <jeffpc/atomic.h>
 #include <jeffpc/sexpr.h>
-#include <jeffpc/str.h>
 #include <jeffpc/io.h>
 #include <jeffpc/qstring.h>
 
@@ -82,9 +81,9 @@
 #define COMMENT_EMPTY		"x"
 
 static const struct nvl_convert_info comment_convert[] = {
-	{ .name = COMMENT_DATE,		.tgt_type = NVT_INT, },
-	{ .name = COMMENT_ID,		.tgt_type = NVT_INT, },
-	{ .name = COMMENT_CAPTCHA,	.tgt_type = NVT_INT, },
+	{ .name = COMMENT_DATE,		.tgt_type = VT_INT, },
+	{ .name = COMMENT_ID,		.tgt_type = VT_INT, },
+	{ .name = COMMENT_CAPTCHA,	.tgt_type = VT_INT, },
 	{ .name = NULL, },
 };
 
@@ -92,12 +91,9 @@
 				   struct str *curdate, struct str *ip,
 				   struct str *url)
 {
-	struct val *url_val;
 	struct val *lv;
 	struct str *str;
 
-	url_val = url ? VAL_ALLOC_STR(url) : NULL;
-
 	/*
 	 * We're looking for a list looking something like:
 	 *
@@ -110,12 +106,12 @@
 	 */
 
 	lv = sexpr_args_to_list(6,
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("author"), VAL_ALLOC_STR(author)),
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("email"), VAL_ALLOC_STR(email)),
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("time"), VAL_ALLOC_STR(curdate)),
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("ip"), VAL_ALLOC_STR(ip)),
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("url"), url_val),
-				VAL_ALLOC_CONS(VAL_ALLOC_SYM_CSTR("moderated"), VAL_ALLOC_BOOL(false)));
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("author")), str_cast_to_val(author)),
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("email")), str_cast_to_val(email)),
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("time")), str_cast_to_val(curdate)),
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("ip")), str_cast_to_val(ip)),
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("url")), str_cast_to_val(url)),
+				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("moderated")), VAL_ALLOC_BOOL(false)));
 
 	str = sexpr_dump(lv, false);
 
@@ -207,7 +203,7 @@
 		goto err;
 	}
 
-	ret = write_file(lisppath, meta->str, str_len(meta));
+	ret = write_file(lisppath, str_cstr(meta), str_len(meta));
 
 	str_putref(meta);
 
@@ -420,13 +416,13 @@
 	int id;
 
 	if (nvl_exists_type(req->scgi->request.headers, SCGI_HTTP_USER_AGENT,
-			    NVT_STR)) {
+			    VT_STR)) {
 		DBG("Missing user agent...");
 		return USERAGENT_MISSING;
 	}
 
 	if (nvl_exists_type(req->scgi->request.headers, SCGI_REMOTE_ADDR,
-			    NVT_STR)) {
+			    VT_STR)) {
 		DBG("Missing remote addr...");
 		return INTERNAL_ERR;
 	}
@@ -450,7 +446,7 @@
 		goto err;
 	}
 
-	ret = nvl_convert(qs, comment_convert);
+	ret = nvl_convert(qs, comment_convert, false);
 	if (ret) {
 		DBG("Failed to convert nvlist types: %s", xstrerror(ret));
 		goto err;
--- a/config.cmake	Sun Aug 06 15:48:33 2017 +0300
+++ b/config.cmake	Sun Dec 30 13:23:15 2018 -0500
@@ -22,8 +22,6 @@
 
 include(CheckFunctionExists)
 
-check_function_exists(reallocarray HAVE_REALLOCARRAY)
-
 set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules")
 find_package(jeffpc)
 
--- a/config.h.in	Sun Aug 06 15:48:33 2017 +0300
+++ b/config.h.in	Sun Dec 30 13:23:15 2018 -0500
@@ -23,8 +23,6 @@
 #ifndef __CONFIG_H
 #define __CONFIG_H
 
-#cmakedefine HAVE_REALLOCARRAY
-
 /* settings */
 #cmakedefine DEFAULT_SCGI_PORT		${DEFAULT_SCGI_PORT}
 
--- a/daemon.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/daemon.c	Sun Dec 30 13:23:15 2018 -0500
@@ -28,7 +28,6 @@
 #include <jeffpc/error.h>
 #include <jeffpc/atomic.h>
 #include <jeffpc/val.h>
-#include <jeffpc/str.h>
 #include <jeffpc/types.h>
 #include <jeffpc/scgisvc.h>
 
@@ -43,18 +42,31 @@
 #include "version.h"
 #include "debug.h"
 
-static void process_request(struct scgi *scgi, void *private)
+static int init_request(struct scgi *scgi, void *private)
 {
-	struct req req;
+	struct req *req;
 
-	req_init(&req);
+	req = malloc(sizeof(struct req));
+	if (!req)
+		return -ENOMEM;
+
+	req_init(req, scgi);
+
+	scgi->private = req;
 
-	req.scgi = scgi;
+	return 0;
+}
 
-	req_dispatch(&req);
+static void deinit_request(struct scgi *scgi)
+{
+	req_destroy(scgi->private);
+	free(scgi->private);
+}
 
-	req_output(&req);
-	req_destroy(&req);
+static void process_request(struct scgi *scgi)
+{
+	req_dispatch(scgi->private);
+	req_output(scgi->private);
 }
 
 static int drop_privs(void)
@@ -106,6 +118,11 @@
 /* the main daemon process */
 static int main_blahgd(int argc, char **argv, int mathfd)
 {
+	static const struct scgiops ops = {
+		.init = init_request,
+		.process = process_request,
+		.deinit = deinit_request,
+	};
 	int ret;
 
 	/* drop unneeded privs */
@@ -129,7 +146,7 @@
 		goto err;
 
 	ret = scgisvc(NULL, config.scgi_port, config.scgi_threads,
-		      process_request, NULL);
+		      &ops, NULL);
 	if (ret)
 		goto err;
 
@@ -158,11 +175,11 @@
 
 	ASSERT0(putenv("TZ=UTC"));
 
+	jeffpc_init(&init_ops);
+
 	cmn_err(CE_INFO, "blahgd version %s", version_string);
 	cmn_err(CE_INFO, "libjeffpc version %s", jeffpc_version);
 
-	jeffpc_init(&init_ops);
-
 	ret = config_load((argc >= 2) ? argv[1] : NULL);
 	if (ret)
 		goto out;
--- a/debug.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/debug.h	Sun Dec 30 13:23:15 2018 -0500
@@ -28,6 +28,8 @@
 
 #define DBG(...)	cmn_err(CE_DEBUG, __VA_ARGS__)
 
+extern void set_session(uint32_t id);
+
 extern struct jeffpc_ops init_ops;
 
 #endif
--- a/error.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/error.c	Sun Dec 30 13:23:15 2018 -0500
@@ -26,6 +26,10 @@
 #include <syslog.h>
 #include <stdarg.h>
 
+/* session info */
+static __thread char session_str[64];
+static __thread uint32_t session_id;
+
 static void mylog(int level, const char *fmt, va_list ap)
 {
 	char msg[512];
@@ -38,6 +42,22 @@
 	syslog(LOG_LOCAL0 | level, "%s", msg);
 }
 
+void set_session(uint32_t id)
+{
+	session_id = id;
+}
+
+static const char *get_session(void)
+{
+	if (!session_id)
+		return "";
+
+	snprintf(session_str, sizeof(session_str), " {%u}", session_id);
+
+	return session_str;
+}
+
 struct jeffpc_ops init_ops = {
 	.log = mylog,
+	.get_session = get_session,
 };
--- a/file_cache.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/file_cache.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2014-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -20,7 +20,6 @@
  * SOFTWARE.
  */
 
-#include <sys/avl.h>
 #include <fcntl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -28,22 +27,25 @@
 #include <stdbool.h>
 #include <port.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/synch.h>
 #include <jeffpc/thread.h>
 #include <jeffpc/refcnt.h>
 #include <jeffpc/io.h>
 #include <jeffpc/list.h>
 #include <jeffpc/mem.h>
+#include <jeffpc/rbtree.h>
 
 #include "file_cache.h"
 #include "utils.h"
-#include "iter.h"
 #include "debug.h"
 
 #define FILE_EVENTS	(FILE_MODIFIED | FILE_ATTRIB)
 
-static avl_tree_t file_cache;
+static LOCK_CLASS(file_lock_lc);
+static LOCK_CLASS(file_node_lc);
+
+static struct rb_tree file_cache;
 static struct lock file_lock;
 
 static int filemon_port;
@@ -60,7 +62,7 @@
 
 struct file_node {
 	char *name;			/* the filename */
-	avl_node_t node;
+	struct rb_node node;
 	refcnt_t refcnt;
 	struct lock lock;
 
@@ -159,7 +161,7 @@
 	 * disk.
 	 */
 	MXLOCK(&file_lock);
-	avl_remove(&file_cache, node);
+	rb_remove(&file_cache, node);
 	MXUNLOCK(&file_lock);
 
 	MXUNLOCK(&node->lock);
@@ -225,10 +227,10 @@
 {
 	int ret;
 
-	MXINIT(&file_lock);
+	MXINIT(&file_lock, &file_lock_lc);
 
-	avl_create(&file_cache, filename_cmp, sizeof(struct file_node),
-		   offsetof(struct file_node, node));
+	rb_create(&file_cache, filename_cmp, sizeof(struct file_node),
+		  offsetof(struct file_node, node));
 
 	file_node_cache = mem_cache_create("file-node-cache",
 					   sizeof(struct file_node), 0);
@@ -262,7 +264,7 @@
 	node->contents = NULL;
 	node->needs_reload = true;
 
-	MXINIT(&node->lock);
+	MXINIT(&node->lock, &file_node_lc);
 	refcnt_init(&node->refcnt, 1);
 	list_create(&node->callbacks, sizeof(struct file_callback),
 		    offsetof(struct file_callback, list));
@@ -326,10 +328,14 @@
 	}
 
 	node->contents = str_alloc(tmp);
-	if (!node->contents) {
-		DBG("file (%s) str_alloc error", node->name);
+	if (IS_ERR(node->contents)) {
+		int ret = PTR_ERR(node->contents);
+
+		DBG("file (%s) str_alloc error: %s", node->name, xstrerror(ret));
 		free(tmp);
-		return -ENOMEM;
+
+		node->contents = NULL;
+		return ret;
 	}
 
 	node->needs_reload = false;
@@ -359,14 +365,14 @@
 	struct file_node *out, *tmp;
 	struct file_node key;
 	struct str *str;
-	avl_index_t where;
+	struct rb_cookie where;
 	int ret;
 
 	key.name = (char *) name;
 
 	/* do we have it? */
 	MXLOCK(&file_lock);
-	out = avl_find(&file_cache, &key, NULL);
+	out = rb_find(&file_cache, &key, NULL);
 	fn_getref(out);
 	MXUNLOCK(&file_lock);
 
@@ -391,7 +397,7 @@
 
 	/* ...and insert it into the cache */
 	MXLOCK(&file_lock);
-	tmp = avl_find(&file_cache, &key, &where);
+	tmp = rb_find(&file_cache, &key, &where);
 	if (tmp) {
 		/*
 		 * uh oh, someone beat us to it; free our copy & return
@@ -412,7 +418,7 @@
 	/* get a ref for the cache */
 	fn_getref(out);
 
-	avl_insert(&file_cache, out, where);
+	rb_insert_here(&file_cache, out, &where);
 
 	MXUNLOCK(&file_lock);
 	MXUNLOCK(&out->lock);
@@ -447,11 +453,11 @@
 void uncache_all_files(void)
 {
 	struct file_node *cur;
-	void *cookie;
+	struct rb_cookie cookie;
 
 	MXLOCK(&file_lock);
-	cookie = NULL;
-	while ((cur = avl_destroy_nodes(&file_cache, &cookie)))
+	memset(&cookie, 0, sizeof(cookie));
+	while ((cur = rb_destroy_nodes(&file_cache, &cookie)))
 		fn_putref(cur);
 	MXUNLOCK(&file_lock);
 }
--- a/index.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/index.c	Sun Dec 30 13:23:15 2018 -0500
@@ -121,8 +121,6 @@
 
 int blahg_index(struct req *req, int page)
 {
-	page = max(page, 0);
-
 	__store_title(&req->vars, "Blahg", false);
 	__store_pages(&req->vars, page);
 
@@ -136,15 +134,29 @@
 	return 0;
 }
 
-static int validate_arch_id(int arch)
+static bool valid_arch_id(uint64_t arch)
 {
-	int y = arch / 100;
-	int m = arch % 100;
+	uint64_t y = arch / 100;
+	uint64_t m = arch % 100;
 
 	return (m >= 1) && (m <= 12) && (y >= 1970) && (y < 2100);
 }
 
-int blahg_archive(struct req *req, int m, int page)
+static int get_arch_id(struct req *req)
+{
+	const int default_arch_id = 197001;
+	uint64_t tmp;
+
+	if (nvl_lookup_int(req->scgi->request.query, "m", &tmp))
+		return default_arch_id;
+
+	if (!valid_arch_id(tmp))
+		return default_arch_id;
+
+	return tmp;
+}
+
+int blahg_archive(struct req *req, int page)
 {
 	static const char *months[12] = {
 		"January", "February", "March", "April", "May", "June",
@@ -153,17 +165,15 @@
 	};
 
 	char nicetitle[32];
+	int m;
 
-	if (!validate_arch_id(m))
-		m = 197001;
+	m = get_arch_id(req);
 
 	snprintf(nicetitle, sizeof(nicetitle), "%d » %s", m / 100,
 		 months[(m % 100) - 1]);
 
 	req_head(req, "Content-Type", "text/html");
 
-	page = max(page, 0);
-
 	__store_title(&req->vars, nicetitle, true);
 	__store_pages(&req->vars, page);
 	__store_archid(&req->vars, m);
--- a/iter.h	Sun Aug 06 15:48:33 2017 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2014-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#ifndef __ITER_H
-#define __ITER_H
-
-#include <stddef.h>
-
-#define avl_for_each(tree, pos) \
-	for (pos = avl_first(tree); pos; pos = AVL_NEXT(tree, pos))
-
-#endif
--- a/lisplint.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/lisplint.c	Sun Dec 30 13:23:15 2018 -0500
@@ -25,7 +25,6 @@
 #include <jeffpc/jeffpc.h>
 #include <jeffpc/error.h>
 #include <jeffpc/sexpr.h>
-#include <jeffpc/str.h>
 #include <jeffpc/val.h>
 
 #include "utils.h"
--- a/listing.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/listing.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2016 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
 
 #include <stdlib.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 
 #include "post.h"
 #include "mangle.h"
--- a/math.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/math.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2014-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,7 @@
 
 #include <sha1.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/synch.h>
 #include <jeffpc/error.h>
 #include <jeffpc/atomic.h>
@@ -47,6 +47,7 @@
  *     process doing its job; it can chdir as needed
  */
 
+static LOCK_CLASS(math_lc);
 static struct lock lock;
 
 static int pipefd = -1;
@@ -369,5 +370,5 @@
 
 	pipefd = fd;
 
-	MXINIT(&lock);
+	MXINIT(&lock, &math_lc);
 }
--- a/math.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/math.h	Sun Dec 30 13:23:15 2018 -0500
@@ -25,7 +25,7 @@
 
 #include <stdbool.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 
 extern void init_math(int fd);
 extern int render_math_processor(int fd);
--- a/nvl.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/nvl.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2015-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -34,15 +34,3 @@
 
 	return out;
 }
-
-const char *pair2str(const struct nvpair *pair)
-{
-	struct str *str;
-
-	str = nvpair_value_str(pair);
-	ASSERT(!IS_ERR(str));
-
-	/* FIXME: we are leaking a refence */
-
-	return str_cstr(str);
-}
--- a/nvl.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/nvl.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2015-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,6 @@
 #include <stdbool.h>
 #include <jeffpc/nvl.h>
 
-extern const char *pair2str(const struct nvpair *pair);
 extern uint64_t pair2int(const struct nvpair *pair);
 
 #endif
--- a/parse.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/parse.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012-2016 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2012-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +24,6 @@
 #define __PARSE_H
 
 #include <jeffpc/error.h>
-#include <jeffpc/str.h>
 #include <jeffpc/sexpr.h>
 
 #include "req.h"
--- a/pipeline.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/pipeline.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2013-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -133,12 +133,18 @@
 			out = str_of_int(val->i);
 			break;
 		case VT_STR:
-			out = cvt(str_cstr(val->str));
+			out = cvt(str_cstr(val_cast_to_str(val)));
 			break;
+		case VT_NULL:
+			val_putref(val);
+			return str_cast_to_val(str_empty_string());
+		case VT_BLOB:
 		case VT_SYM:
 		case VT_CONS:
 		case VT_BOOL:
 		case VT_CHAR:
+		case VT_ARRAY:
+		case VT_NVL:
 			panic("%s called with value type %d", __func__,
 			      val->type);
 	}
@@ -147,7 +153,7 @@
 
 	ASSERT(out);
 
-	return VAL_ALLOC_CSTR(out);
+	return VAL_ALLOC_STR(out);
 }
 
 static struct val *urlescape_fxn(struct val *val)
@@ -174,7 +180,7 @@
 
 	val_putref(val);
 
-	return VAL_DUP_CSTR(buf);
+	return VAL_DUP_STR(buf);
 }
 
 static struct val *time_fxn(struct val *val)
--- a/post.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/post.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -38,7 +38,6 @@
 #include <jeffpc/io.h>
 #include <jeffpc/mem.h>
 
-#include "iter.h"
 #include "post.h"
 #include "vars.h"
 #include "req.h"
@@ -52,7 +51,9 @@
 static struct mem_cache *post_cache;
 static struct mem_cache *comment_cache;
 
-static void post_remove_all_tags(avl_tree_t *taglist);
+static LOCK_CLASS(post_lc);
+
+static void post_remove_all_tags(struct rb_tree *taglist);
 static void post_remove_all_comments(struct post *post);
 
 static int tag_cmp(const void *va, const void *vb)
@@ -93,41 +94,31 @@
 	post_unlock(post);
 }
 
-static void post_add_tags(avl_tree_t *taglist, struct val *list)
+/* consumes the struct val reference */
+static void post_add_tags(struct rb_tree *taglist, struct val *list)
 {
-	struct post_tag *tag;
+	struct val *tagval;
+	struct val *tmp;
 
-	/* tags list not present in metadata */
-	if (!list)
-		return;
-
-	for (; list; list = sexpr_cdr(list)) {
-		struct val *tagval;
-		struct str *tagname;
-
-		tagval = sexpr_car(val_getref(list));
+	sexpr_for_each_noref(tagval, tmp, list) {
+		struct post_tag *tag;
 
 		/* sanity check */
 		ASSERT3U(tagval->type, ==, VT_STR);
 
-		/* get the tag name */
-		tagname = str_getref(tagval->str);
-
-		/* release the tag value */
-		val_putref(tagval);
-
 		tag = malloc(sizeof(struct post_tag));
 		ASSERT(tag);
 
-		tag->tag = tagname;
-		ASSERT(tag->tag);
+		tag->tag = val_getref_str(tagval);
 
-		if (safe_avl_add(taglist, tag)) {
+		if (rb_insert(taglist, tag)) {
 			/* found a duplicate */
 			str_putref(tag->tag);
 			free(tag);
 		}
 	}
+
+	val_putref(list);
 }
 
 static void post_remove_all_comments(struct post *post)
@@ -213,25 +204,21 @@
 	str_putref(meta);
 }
 
+/* consumes the struct val reference */
 static void post_add_comments(struct post *post, struct val *list)
 {
-	if (!list)
-		return;
+	struct val *val;
+	struct val *tmp;
 
-	for (; list; list = sexpr_cdr(list)) {
-		struct val *val;
-
-		val = sexpr_car(val_getref(list));
-
+	sexpr_for_each_noref(val, tmp, list) {
 		/* sanity check */
 		ASSERT3U(val->type, ==, VT_INT);
 
 		/* add the comment */
 		post_add_comment(post, val->i);
+	}
 
-		/* release the value */
-		val_putref(val);
-	}
+	val_putref(list);
 }
 
 static int __do_load_post_body_fmt3(struct post *post, const struct str *input)
@@ -485,14 +472,14 @@
 	post->preview = preview;
 	post->needs_refresh = true;
 
-	avl_create(&post->tags, tag_cmp, sizeof(struct post_tag),
-		    offsetof(struct post_tag, node));
-	avl_create(&post->cats, tag_cmp, sizeof(struct post_tag),
-		    offsetof(struct post_tag, node));
+	rb_create(&post->tags, tag_cmp, sizeof(struct post_tag),
+		  offsetof(struct post_tag, node));
+	rb_create(&post->cats, tag_cmp, sizeof(struct post_tag),
+		  offsetof(struct post_tag, node));
 	list_create(&post->comments, sizeof(struct comment),
 		    offsetof(struct comment, list));
 	refcnt_init(&post->refcnt, 1);
-	MXINIT(&post->lock);
+	MXINIT(&post->lock, &post_lc);
 
 	if ((err = __refresh(post)))
 		goto err_free;
@@ -510,19 +497,19 @@
 	return NULL;
 }
 
-static void post_remove_all_tags(avl_tree_t *taglist)
+static void post_remove_all_tags(struct rb_tree *taglist)
 {
 	struct post_tag *tag;
-	void *cookie;
+	struct rb_cookie cookie;
 
-	cookie = NULL;
-	while ((tag = avl_destroy_nodes(taglist, &cookie))) {
+	memset(&cookie, 0, sizeof(cookie));
+	while ((tag = rb_destroy_nodes(taglist, &cookie))) {
 		str_putref(tag->tag);
 		free(tag);
 	}
 
-	avl_create(taglist, tag_cmp, sizeof(struct post_tag),
-		    offsetof(struct post_tag, node));
+	rb_create(taglist, tag_cmp, sizeof(struct post_tag),
+		  offsetof(struct post_tag, node));
 }
 
 void post_destroy(struct post *post)
--- a/post.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/post.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -25,16 +25,16 @@
 
 #include <time.h>
 #include <stdbool.h>
-#include <sys/avl.h>
 
 #include <jeffpc/synch.h>
 #include <jeffpc/refcnt.h>
 #include <jeffpc/list.h>
+#include <jeffpc/rbtree.h>
 
 #include "vars.h"
 
 struct post_tag {
-	avl_node_t node;
+	struct rb_node node;
 	struct str *tag;
 };
 
@@ -66,8 +66,8 @@
 	unsigned int fmt;
 
 	/* from 'post_tags' table */
-	avl_tree_t tags;
-	avl_tree_t cats;
+	struct rb_tree tags;
+	struct rb_tree cats;
 
 	/* from 'comments' table */
 	struct list comments;
--- a/post_fmt3.l	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_fmt3.l	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2011-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -29,8 +29,6 @@
 %{
 #include <stdlib.h>
 
-#include <jeffpc/str.h>
-
 #include "parse.h"
 
 #include "post_fmt3.tab.h"
@@ -73,23 +71,13 @@
 "\\tag{"			{ BEGIN(SPECIALCMD); return TAGSTART; }
 "\\category{"			{ BEGIN(SPECIALCMD); return CATSTART; }
 "\\twitterimg{"			{ BEGIN(SPECIALCMD); return TWITTERIMGSTART; }
+"\\twitterphoto{"		{ BEGIN(SPECIALCMD); return TWITTERPHOTOSTART; }
 <SPECIALCMD>"}"			{ BEGIN(INITIAL); return SPECIALCMDEND; }
 <SPECIALCMD>.			{ yylval->ptr = STR_DUP(yytext); return VERBTEXT; }
 
 "$"			{ BEGIN(MATH); return MATHSTART; }
 <MATH>"$"		{ BEGIN(INITIAL); return MATHEND; }
-<MATH>"\\"		{ return BSLASH; }
-<MATH>"_"		{ return USCORE; }
-<MATH>"^"		{ return CARRET; }
-<MATH>"+"		{ return PLUS; }
-<MATH>"-"		{ return MINUS; }
-<MATH>"*"		{ return ASTERISK; }
-<MATH>"/"		{ return SLASH; }
-<MATH>"{"		{ return OCURLY; }
-<MATH>"}"		{ return CCURLY; }
-<MATH>"("		{ return OPAREN; }
-<MATH>")"		{ return CPAREN; }
-<MATH>"~"		{ return TILDE; }
+<MATH>[\\{}()*/~^_+-]	{ return *yytext; }
 <MATH>[=<>]		{ yylval->ptr = STR_DUP(yytext); return EQLTGT; }
 <MATH>[A-Za-z0-9]+	{ yylval->ptr = STR_DUP(yytext); return WORD; }
 <MATH>[".,=<>!:;?@*#]	{ yylval->ptr = STR_DUP(yytext); return SCHAR; }
@@ -107,29 +95,20 @@
 			}
 \n			{
 				inc_lineno(yyscanner);
-				return NLINE;
+				return *yytext;
 			}
 %[^\n]*			{ /* tex comment */ }
 [ \t]+			{ yylval->ptr = STR_DUP(yytext); return WSPACE; }
-"\\"			{ return BSLASH; }
-"{"			{ return OCURLY; }
-"}"			{ return CCURLY; }
-"["			{ return OBRACE; }
-"]"			{ return CBRACE; }
-"&"			{ return AMP; }
-"_"			{ return USCORE; }
 "\\%"			{ return PERCENT; }
-"~"			{ return TILDE; }
-"|"			{ return PIPE; }
-"^"			{ return CARRET; }
+[\\{}~&^_[\]]		{ return *yytext; }
 \.{3}			{ return ELLIPSIS; }
 -{1,3}			{ yylval->ptr = STR_DUP(yytext); return DASH; }
 `{1,2}			{ yylval->ptr = STR_DUP(yytext); return OQUOT; }
 '{1,2}			{ yylval->ptr = STR_DUP(yytext); return CQUOT; }
-[".,()/=<>!:;\+?@*#]	{ yylval->ptr = STR_DUP(yytext); return SCHAR; }
-[\xe0-\xef]		{ yylval->ptr = STR_DUP(yytext); return UTF8FIRST3; }
-[\xc0-\xdf]		{ yylval->ptr = STR_DUP(yytext); return UTF8FIRST2; }
-[\x80-\xbf]		{ yylval->ptr = STR_DUP(yytext); return UTF8REST; }
+[.,()/=!:;\+?@*#|]	{ yylval->ptr = STR_DUP(yytext); return CHAR; }
+["<>]			{ yylval->ptr = STR_DUP(yytext); return SCHAR; }
+[\xe0-\xef][\x80-\xbf][\x80-\xbf]	{ yylval->ptr = STR_DUP(yytext); return UTF8CHAR; }
+[\xc0-\xdf][\x80-\xbf]			{ yylval->ptr = STR_DUP(yytext); return UTF8CHAR; }
 [A-Za-z0-9]+		{ yylval->ptr = STR_DUP(yytext); return WORD; }
 .			{ fmt3_error2("post text contains invalid characters", yytext); yyterminate(); }
 %%
--- a/post_fmt3.y	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_fmt3.y	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2011-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -34,7 +34,6 @@
 #include <sys/stat.h>
 
 #include <jeffpc/error.h>
-#include <jeffpc/str.h>
 #include <jeffpc/types.h>
 
 #include "config.h"
@@ -108,7 +107,8 @@
 		case '<': ret = STATIC_STR("&lt;"); break;
 		case '>': ret = STATIC_STR("&gt;"); break;
 		default:
-			return val;
+			panic("%s given an unexpected character '%c'",
+			      __func__, str_cstr(val)[0]);
 	}
 
 	str_putref(val);
@@ -117,23 +117,23 @@
 }
 
 static void special_cmd(struct parser_output *data, struct str **var,
-			struct str *value)
+			struct str *value, bool photo)
 {
 	str_putref(*var);
 
-	*var = value;
+	if (photo)
+		*var = str_cat(3, str_getref(config.photo_base_url),
+			       STATIC_STR("/"),
+			       value);
+	else
+		*var = value;
 }
 
 static void special_cmd_list(struct parser_output *data, struct val **var,
 			     struct str *value)
 {
-	struct val *new;
-
-	/* wrap the string in a value */
-	new = VAL_ALLOC_STR(value);
-
 	/* extend the list */
-	*var = VAL_ALLOC_CONS(new, *var);
+	*var = VAL_ALLOC_CONS(str_cast_to_val(value), *var);
 }
 
 %}
@@ -144,19 +144,13 @@
 
 /* generic tokens */
 %token <ptr> WSPACE
-%token <ptr> DASH OQUOT CQUOT SCHAR
-%token <ptr> UTF8FIRST3 UTF8FIRST2 UTF8REST WORD
-%token SLASH
-%token PIPE
-%token OCURLY CCURLY OBRACE CBRACE
-%token USCORE CARRET ASTERISK
-%token BSLASH PERCENT AMP TILDE ELLIPSIS
-%token PAREND NLINE
+%token <ptr> DASH OQUOT CQUOT SCHAR CHAR
+%token <ptr> UTF8CHAR WORD
+%token PERCENT ELLIPSIS
+%token PAREND
 
 /* math specific tokens */
 %token <ptr> EQLTGT
-%token PLUS MINUS
-%token OPAREN CPAREN
 %token MATHSTART MATHEND
 
 /* verbose & listing environment */
@@ -164,22 +158,23 @@
 %token VERBSTART VERBEND DOLLAR
 %token LISTSTART LISTEND
 %token TITLESTART TAGSTART CATSTART PUBSTART TWITTERIMGSTART
+%token TWITTERPHOTOSTART
 %token SPECIALCMDEND
 
 %type <ptr> paragraphs paragraph thing cmd cmdarg optcmdarg math mexpr
 %type <ptr> verb
 
-%left USCORE CARRET
-%left TILDE
+%left '_' '^'
+%left '~'
 %left EQLTGT
-%left PLUS MINUS
-%left ASTERISK SLASH
+%left '+' '-'
+%left '*' '/'
 
 %%
 
 post : paragraphs PAREND		{ data->stroutput = $1; }
      | paragraphs			{ data->stroutput = $1; }
-     | PAREND				{ data->stroutput = STATIC_STR(""); }
+     | PAREND				{ data->stroutput = str_empty_string(); }
      ;
 
 paragraphs : paragraphs PAREND paragraph	{ $$ = str_cat(4, $1, STATIC_STR("<p>"), $3, STATIC_STR("</p>\n")); }
@@ -191,54 +186,53 @@
           ;
 
 thing : WORD				{ $$ = $1; }
-      | UTF8FIRST2 UTF8REST		{ $$ = str_cat(2, $1, $2); }
-      | UTF8FIRST3 UTF8REST UTF8REST	{ $$ = str_cat(3, $1, $2, $3); }
-      | NLINE				{ $$ = data->texttt_nesting ? STATIC_STR("\n") : STATIC_STR(" "); }
+      | UTF8CHAR			{ $$ = $1; }
+      | '\n'				{ $$ = data->texttt_nesting ? STATIC_STR("\n") : STATIC_STR(" "); }
       | WSPACE				{ $$ = $1; }
-      | PIPE				{ $$ = STATIC_STR("|"); }
-      | ASTERISK			{ $$ = STATIC_STR("*"); }
-      | SLASH				{ $$ = STATIC_STR("/"); }
       | DASH				{ $$ = dash($1); }
       | OQUOT				{ $$ = oquote($1); }
       | CQUOT				{ $$ = cquote($1); }
+      | CHAR				{ $$ = $1; }
       | SCHAR				{ $$ = special_char($1); }
       | ELLIPSIS			{ $$ = STATIC_STR("&hellip;"); }
-      | TILDE				{ $$ = STATIC_STR("&nbsp;"); }
-      | AMP				{ $$ = STATIC_STR("</td><td>"); }
+      | '~'				{ $$ = STATIC_STR("&nbsp;"); }
+      | '&'				{ $$ = STATIC_STR("</td><td>"); }
       | DOLLAR				{ $$ = STATIC_STR("$"); }
       | PERCENT				{ $$ = STATIC_STR("%"); }
-      | BSLASH cmd			{ $$ = $2; }
+      | '\\' cmd			{ $$ = $2; }
       | MATHSTART math MATHEND		{ $$ = render_math($2); }
       | VERBSTART verb VERBEND		{ $$ = str_cat(3, STATIC_STR("</p><p>"), $2, STATIC_STR("</p><p>")); }
       | LISTSTART verb LISTEND		{ $$ = str_cat(3, STATIC_STR("</p><pre>"),
 						       listing_str($2),
 						       STATIC_STR("</pre><p>")); }
-      | TITLESTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd(data, &data->sc_title, $2); }
-      | PUBSTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd(data, &data->sc_pub, $2); }
+      | TITLESTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd(data, &data->sc_title, $2, false); }
+      | PUBSTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd(data, &data->sc_pub, $2, false); }
       | TAGSTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd_list(data, &data->sc_tags, $2); }
       | CATSTART verb SPECIALCMDEND	{ $$ = NULL; special_cmd_list(data, &data->sc_cats, $2); }
       | TWITTERIMGSTART verb SPECIALCMDEND
-					{ $$ = NULL; special_cmd(data, &data->sc_twitter_img, $2); }
+					{ $$ = NULL; special_cmd(data, &data->sc_twitter_img, $2, false); }
+      | TWITTERPHOTOSTART verb SPECIALCMDEND
+					{ $$ = NULL; special_cmd(data, &data->sc_twitter_img, $2, true); }
       ;
 
 cmd : WORD optcmdarg cmdarg	{ $$ = process_cmd(data, $1, $3, $2); }
     | WORD cmdarg		{ $$ = process_cmd(data, $1, $2, NULL); }
     | WORD			{ $$ = process_cmd(data, $1, NULL, NULL); }
-    | BSLASH			{ $$ = STATIC_STR("<br/>"); }
-    | OCURLY			{ $$ = STATIC_STR("{"); }
-    | CCURLY			{ $$ = STATIC_STR("}"); }
-    | OBRACE			{ $$ = STATIC_STR("["); }
-    | CBRACE			{ $$ = STATIC_STR("]"); }
-    | AMP			{ $$ = STATIC_STR("&amp;"); }
-    | USCORE			{ $$ = STATIC_STR("_"); }
-    | CARRET			{ $$ = STATIC_STR("^"); }
-    | TILDE			{ $$ = STATIC_STR("~"); }
+    | '\\'			{ $$ = STATIC_STR("<br/>"); }
+    | '{'			{ $$ = STATIC_STR("{"); }
+    | '}'			{ $$ = STATIC_STR("}"); }
+    | '['			{ $$ = STATIC_STR("["); }
+    | ']'			{ $$ = STATIC_STR("]"); }
+    | '&'			{ $$ = STATIC_STR("&amp;"); }
+    | '_'			{ $$ = STATIC_STR("_"); }
+    | '^'			{ $$ = STATIC_STR("^"); }
+    | '~'			{ $$ = STATIC_STR("~"); }
     ;
 
-optcmdarg : OBRACE paragraph CBRACE	{ $$ = $2; }
+optcmdarg : '[' paragraph ']'	{ $$ = $2; }
           ;
 
-cmdarg : OCURLY paragraph CCURLY	{ $$ = $2; }
+cmdarg : '{' paragraph '}'	{ $$ = $2; }
        ;
 
 verb : verb VERBTEXT			{ $$ = str_cat(2, $1, $2); }
@@ -250,19 +244,20 @@
 
 mexpr : WORD				{ $$ = $1; }
       | WSPACE				{ $$ = $1; }
+      | CHAR				{ $$ = $1; }
       | SCHAR				{ $$ = $1; }
       | mexpr EQLTGT mexpr 		{ $$ = str_cat(3, $1, $2, $3); }
-      | mexpr USCORE mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("_"), $3); }
-      | mexpr CARRET mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("^"), $3); }
-      | mexpr PLUS mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("+"), $3); }
-      | mexpr MINUS mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("-"), $3); }
-      | mexpr ASTERISK mexpr	 	{ $$ = str_cat(3, $1, STATIC_STR("*"), $3); }
-      | mexpr SLASH mexpr	 	{ $$ = str_cat(3, $1, STATIC_STR("/"), $3); }
-      | mexpr TILDE mexpr		{ $$ = str_cat(3, $1, STATIC_STR("~"), $3); }
-      | BSLASH WORD			{ $$ = str_cat(2, STATIC_STR("\\"), $2); }
-      | BSLASH USCORE			{ $$ = STATIC_STR("\\_"); }
-      | OPAREN math CPAREN		{ $$ = str_cat(3, STATIC_STR("("), $2, STATIC_STR(")")); }
-      | OCURLY math CCURLY		{ $$ = str_cat(3, STATIC_STR("{"), $2, STATIC_STR("}")); }
+      | mexpr '_' mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("_"), $3); }
+      | mexpr '^' mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("^"), $3); }
+      | mexpr '+' mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("+"), $3); }
+      | mexpr '-' mexpr 		{ $$ = str_cat(3, $1, STATIC_STR("-"), $3); }
+      | mexpr '*' mexpr		 	{ $$ = str_cat(3, $1, STATIC_STR("*"), $3); }
+      | mexpr '/' mexpr	 		{ $$ = str_cat(3, $1, STATIC_STR("/"), $3); }
+      | mexpr '~' mexpr			{ $$ = str_cat(3, $1, STATIC_STR("~"), $3); }
+      | '\\' WORD			{ $$ = str_cat(2, STATIC_STR("\\"), $2); }
+      | '\\' '_'			{ $$ = STATIC_STR("\\_"); }
+      | '(' math ')'			{ $$ = str_cat(3, STATIC_STR("("), $2, STATIC_STR(")")); }
+      | '{' math '}'			{ $$ = str_cat(3, STATIC_STR("{"), $2, STATIC_STR("}")); }
       ;
 
 %%
--- a/post_fmt3_cmds.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_fmt3_cmds.c	Sun Dec 30 13:23:15 2018 -0500
@@ -22,7 +22,7 @@
 
 #include <stdbool.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/types.h>
 
 #include "config.h"
@@ -346,7 +346,7 @@
 	str_putref(txt);
 	str_putref(opt);
 
-	return STATIC_STR("");
+	return str_empty_string();
 }
 
 typedef enum {
--- a/post_fmt4_ast.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_fmt4_ast.c	Sun Dec 30 13:23:15 2018 -0500
@@ -26,7 +26,6 @@
 #include <jeffpc/error.h>
 
 #include "post_fmt4_ast.h"
-#include "iter.h"
 #include "ast_opt.h"
 
 /*
--- a/post_index.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_index.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2015-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -24,12 +24,11 @@
 #include <stddef.h>
 #include <sys/sysmacros.h>
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/error.h>
 #include <jeffpc/mem.h>
 
 #include "post.h"
-#include "iter.h"
 #include "utils.h"
 
 /*
@@ -49,14 +48,14 @@
  *
  * To maximize code reuse, the by-tag and by-category trees contain struct
  * post_subindex nodes for each (unique) tag/category.  Those nodes contain
- * AVL trees of their own with struct post_index_entry elements mapping
- * <timestamp, post id> (much like the by time index) to a post.
+ * binary search trees of their own with struct post_index_entry elements
+ * mapping <timestamp, post id> (much like the by time index) to a post.
  *
  * Because this isn't complex enough, we also keep a linked list of all the
  * tag entries rooted in the global index tree node.
  *
  *      +--------------------------------+
- *      | index_global AVL tree          |
+ *      | index_global tree              |
  *      |+-------------------------+     |   +------+
  *      || post global index entry | ... |   | post |
  *  +--->|   by_tag  by_cat  post  |     |   +------+
@@ -68,11 +67,11 @@
  *  | +--------+       +--------------------------+
  *  | |                                           |
  *  | | +-------------------------------------+   |
- *  | | |  index_by_tag AVL tree              |   |
+ *  | | |  index_by_tag tree                  |   |
  *  | | | +-----------------------------+     |   |
  *  | | | |  post subindex              |     |   |
  *  | | | | +-------------------------+ |     |   |
- *  | | | | |  subindex AVL tree      | |     |   |
+ *  | | | | |  subindex tree          | |     |   |
  *  | | | | | +-----------------+     | |     |   |
  *  | +-----> |post index entry | ... | |     |   |
  *  |   | | | |  global  xref   |     | | ... |   |
@@ -87,11 +86,11 @@
  *  | +-------------------------------------------+
  *  | |
  *  | | +-------------------------------------+
- *  | | |  index_by_cat AVL tree              |
+ *  | | |  index_by_cat tree                  |
  *  | | | +-----------------------------+     |
  *  | | | |  post subindex              |     |
  *  | | | | +-------------------------+ |     |
- *  | | | | |  subindex AVL tree      | |     |
+ *  | | | | |  subindex tree          | |     |
  *  | | | | | +-----------------+     | |     |
  *  | +-----> |post index entry | ... | |     |
  *  |   | | | |  global  xref   |     | | ... |
@@ -104,7 +103,7 @@
  *  |   +-------------------------------------+
  *  |
  *  |   +-------------------------+
- *  |   | index_by_time AVL tree  |
+ *  |   | index_by_time tree      |
  *  |   |+------------------+     |
  *  |   || post index entry | ... |
  *  |   ||   global         |     |
@@ -122,7 +121,7 @@
 };
 
 struct post_global_index_entry {
-	avl_node_t node;
+	struct rb_node node;
 
 	/* key */
 	unsigned int id;
@@ -139,7 +138,7 @@
 };
 
 struct post_index_entry {
-	avl_node_t node;
+	struct rb_node node;
 
 	struct post_global_index_entry *global;
 
@@ -161,21 +160,22 @@
 };
 
 struct post_subindex {
-	avl_node_t index;
+	struct rb_node index;
 
 	/* key */
 	struct str *name;
 
 	/* value */
-	avl_tree_t subindex;
+	struct rb_tree subindex;
 };
 
-static avl_tree_t index_global;
-static avl_tree_t index_by_time;
-static avl_tree_t index_by_tag;
-static avl_tree_t index_by_cat;
+static struct rb_tree index_global;
+static struct rb_tree index_by_time;
+static struct rb_tree index_by_tag;
+static struct rb_tree index_by_cat;
 
 static struct lock index_lock;
+static LOCK_CLASS(index_lock_lc);
 
 static struct mem_cache *index_entry_cache;
 static struct mem_cache *global_index_entry_cache;
@@ -235,27 +235,27 @@
 	return 0;
 }
 
-static void init_index_tree(avl_tree_t *tree)
+static void init_index_tree(struct rb_tree *tree)
 {
-	avl_create(tree, post_index_cmp, sizeof(struct post_index_entry),
-		   offsetof(struct post_index_entry, node));
+	rb_create(tree, post_index_cmp, sizeof(struct post_index_entry),
+		  offsetof(struct post_index_entry, node));
 }
 
 void init_post_index(void)
 {
-	avl_create(&index_global, post_global_index_cmp,
-		   sizeof(struct post_global_index_entry),
-		   offsetof(struct post_global_index_entry, node));
+	rb_create(&index_global, post_global_index_cmp,
+		  sizeof(struct post_global_index_entry),
+		  offsetof(struct post_global_index_entry, node));
 
 	init_index_tree(&index_by_time);
 
 	/* set up the by-tag/category indexes */
-	avl_create(&index_by_tag, post_tag_cmp, sizeof(struct post_subindex),
-		   offsetof(struct post_subindex, index));
-	avl_create(&index_by_cat, post_tag_cmp, sizeof(struct post_subindex),
-		   offsetof(struct post_subindex, index));
+	rb_create(&index_by_tag, post_tag_cmp, sizeof(struct post_subindex),
+		  offsetof(struct post_subindex, index));
+	rb_create(&index_by_cat, post_tag_cmp, sizeof(struct post_subindex),
+		  offsetof(struct post_subindex, index));
 
-	MXINIT(&index_lock);
+	MXINIT(&index_lock, &index_lock_lc);
 
 	index_entry_cache = mem_cache_create("index-entry-cache",
 					     sizeof(struct post_index_entry),
@@ -272,14 +272,15 @@
 	ASSERT(!IS_ERR(subindex_cache));
 }
 
-static avl_tree_t *__get_subindex(avl_tree_t *index, struct str *tagname)
+static struct rb_tree *__get_subindex(struct rb_tree *index,
+				       struct str *tagname)
 {
 	struct post_subindex *ret;
 	struct post_subindex key = {
 		.name = tagname,
 	};
 
-	ret = avl_find(index, &key, NULL);
+	ret = rb_find(index, &key, NULL);
 	if (!ret)
 		return NULL;
 
@@ -296,7 +297,7 @@
 	struct post *post;
 
 	MXLOCK(&index_lock);
-	ret = avl_find(&index_global, &key, NULL);
+	ret = rb_find(&index_global, &key, NULL);
 	if (ret)
 		post = post_getref(ret->post);
 	else
@@ -312,7 +313,7 @@
 		    int skip, int nposts)
 {
 	struct post_index_entry *cur;
-	avl_tree_t *tree;
+	struct rb_tree *tree;
 	int i;
 
 	MXLOCK(&index_lock);
@@ -331,7 +332,7 @@
 	}
 
 	/* skip over the first (listed) entries as requested */
-	for (cur = avl_last(tree); cur && skip; cur = AVL_PREV(tree, cur)) {
+	for (cur = rb_last(tree); cur && skip; cur = rb_prev(tree, cur)) {
 		if (!cur->global->post->listed)
 			continue; /* don't count non-listed posts */
 
@@ -339,7 +340,7 @@
 	}
 
 	/* get a reference for every post we're returning */
-	for (i = 0; cur && nposts; cur = AVL_PREV(tree, cur)) {
+	for (i = 0; cur && nposts; cur = rb_prev(tree, cur)) {
 		if (!cur->global->post->listed)
 			continue; /* skip non-listed posts */
 
@@ -357,23 +358,23 @@
 	return i;
 }
 
-static int __insert_post_tags(avl_tree_t *index,
+static int __insert_post_tags(struct rb_tree *index,
 			      struct post_global_index_entry *global,
-			      avl_tree_t *taglist, struct list *xreflist,
+			      struct rb_tree *taglist, struct list *xreflist,
 			      enum entry_type type)
 {
 	struct post_index_entry *tag_entry;
 	struct post_subindex *sub;
 	struct post_tag *tag;
-	avl_index_t where;
+	struct rb_cookie where;
 
-	avl_for_each(taglist, tag) {
+	rb_for_each(taglist, tag) {
 		struct post_subindex key = {
 			.name = tag->tag,
 		};
 
 		/* find the right subindex, or... */
-		sub = avl_find(index, &key, &where);
+		sub = rb_find(index, &key, &where);
 		if (!sub) {
 			/* ...allocate one if it doesn't exist */
 			sub = mem_cache_alloc(subindex_cache);
@@ -383,7 +384,7 @@
 			sub->name = str_getref(tag->tag);
 			init_index_tree(&sub->subindex);
 
-			avl_insert(index, sub, where);
+			rb_insert_here(index, sub, &where);
 		}
 
 		/* allocate & add a entry to the subindex */
@@ -395,7 +396,7 @@
 		tag_entry->name   = str_getref(tag->tag);
 		tag_entry->type   = type;
 
-		ASSERT3P(safe_avl_add(&sub->subindex, tag_entry), ==, NULL);
+		ASSERT3P(rb_insert(&sub->subindex, tag_entry), ==, NULL);
 		list_insert_tail(xreflist, tag_entry);
 	}
 
@@ -441,14 +442,14 @@
 	MXLOCK(&index_lock);
 
 	/* add the post to the global index */
-	if (safe_avl_add(&index_global, global)) {
+	if (rb_insert(&index_global, global)) {
 		MXUNLOCK(&index_lock);
 		ret = -EEXIST;
 		goto err_free_by_time;
 	}
 
 	/* add the post to the by-time index */
-	ASSERT3P(safe_avl_add(&index_by_time, by_time), ==, NULL);
+	ASSERT3P(rb_insert(&index_by_time, by_time), ==, NULL);
 
 	ret = __insert_post_tags(&index_by_tag, global, &post->tags,
 				 &global->by_tag, ET_TAG);
@@ -470,7 +471,7 @@
 err_free_tags:
 	// XXX: __remove_post_tags(&index_by_tag, &post->tags);
 
-	avl_remove(&index_by_time, by_time);
+	rb_remove(&index_by_time, by_time);
 
 	MXUNLOCK(&index_lock);
 
@@ -490,7 +491,7 @@
 	struct post_global_index_entry *cur;
 
 	MXLOCK(&index_lock);
-	avl_for_each(&index_global, cur)
+	rb_for_each(&index_global, cur)
 		revalidate_post(cur->post);
 	MXUNLOCK(&index_lock);
 }
@@ -510,7 +511,7 @@
 	MXLOCK(&index_lock);
 
 	if (init) {
-		ret = init(private, avl_numnodes(&index_by_tag));
+		ret = init(private, rb_numnodes(&index_by_tag));
 		if (ret)
 			goto err;
 	}
@@ -523,45 +524,45 @@
 	 */
 	cmin = ~0;
 	cmax = 0;
-	avl_for_each(&index_by_tag, tag) {
-		cmin = MIN(cmin, avl_numnodes(&tag->subindex));
-		cmax = MAX(cmax, avl_numnodes(&tag->subindex));
+	rb_for_each(&index_by_tag, tag) {
+		cmin = MIN(cmin, rb_numnodes(&tag->subindex));
+		cmax = MAX(cmax, rb_numnodes(&tag->subindex));
 	}
 
 	/*
 	 * finally, invoke the step callback for each tag
 	 */
-	avl_for_each(&index_by_tag, tag)
-		step(private, tag->name, avl_numnodes(&tag->subindex),
+	rb_for_each(&index_by_tag, tag)
+		step(private, tag->name, rb_numnodes(&tag->subindex),
 		     cmin, cmax);
 
 err:
 	MXUNLOCK(&index_lock);
 }
 
-static void __free_global_index(avl_tree_t *tree)
+static void __free_global_index(struct rb_tree *tree)
 {
 	struct post_global_index_entry *cur;
-	void *cookie;
+	struct rb_cookie cookie;
 
-	cookie = NULL;
-	while ((cur = avl_destroy_nodes(tree, &cookie))) {
+	memset(&cookie, 0, sizeof(cookie));
+	while ((cur = rb_destroy_nodes(tree, &cookie))) {
 		post_putref(cur->post);
 		list_destroy(&cur->by_tag);
 		list_destroy(&cur->by_cat);
 		mem_cache_free(global_index_entry_cache, cur);
 	}
 
-	avl_destroy(tree);
+	rb_destroy(tree);
 }
 
-static void __free_index(avl_tree_t *tree)
+static void __free_index(struct rb_tree *tree)
 {
 	struct post_index_entry *cur;
-	void *cookie;
+	struct rb_cookie cookie;
 
-	cookie = NULL;
-	while ((cur = avl_destroy_nodes(tree, &cookie))) {
+	memset(&cookie, 0, sizeof(cookie));
+	while ((cur = rb_destroy_nodes(tree, &cookie))) {
 		struct list *xreflist = NULL;
 
 		switch (cur->type) {
@@ -583,22 +584,22 @@
 		mem_cache_free(index_entry_cache, cur);
 	}
 
-	avl_destroy(tree);
+	rb_destroy(tree);
 }
 
-static void __free_tag_index(avl_tree_t *tree)
+static void __free_tag_index(struct rb_tree *tree)
 {
 	struct post_subindex *cur;
-	void *cookie;
+	struct rb_cookie cookie;
 
-	cookie = NULL;
-	while ((cur = avl_destroy_nodes(tree, &cookie))) {
+	memset(&cookie, 0, sizeof(cookie));
+	while ((cur = rb_destroy_nodes(tree, &cookie))) {
 		__free_index(&cur->subindex);
 		str_putref((struct str *) cur->name);
 		mem_cache_free(subindex_cache, cur);
 	}
 
-	avl_destroy(tree);
+	rb_destroy(tree);
 }
 
 void free_all_posts(void)
--- a/post_nv.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/post_nv.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -20,52 +20,37 @@
  * SOFTWARE.
  */
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/list.h>
+#include <jeffpc/mem.h>
 
-#include "iter.h"
 #include "post.h"
 #include "req.h"
 
-static int __tag_val(struct nvlist *post, avl_tree_t *list)
+static int __tag_val(struct nvlist *post, struct rb_tree *list)
 {
 	struct post_tag *cur;
-	struct nvval *tags;
+	struct val **tags;
 	size_t ntags;
 	size_t i;
-	int ret;
-
-	ntags = avl_numnodes(list);
 
-#ifdef HAVE_REALLOCARRAY
-	tags = reallocarray(NULL, ntags, sizeof(struct nvval));
-#else
-	tags = malloc(ntags * sizeof(struct nvval));
-#endif
+	ntags = rb_numnodes(list);
+
+	tags = mem_reallocarray(NULL, ntags, sizeof(struct val *));
 	if (!tags)
 		return -ENOMEM;
 
 	i = 0;
-	avl_for_each(list, cur) {
-		struct nvval *tag = &tags[i++];
-
-		tag->type = NVT_STR;
-		tag->str = str_getref(cur->tag);
-	}
+	rb_for_each(list, cur)
+		tags[i++] = str_getref_val(cur->tag);
 
-	ret = nvl_set_array(post, "tags", tags, ntags);
-	if (ret) {
-		nvval_release_array(tags, ntags);
-		free(tags);
-	}
-
-	return ret;
+	return nvl_set_array(post, "tags", tags, ntags);
 }
 
 static int __com_val(struct nvlist *post, struct list *list)
 {
 	struct comment *cur;
-	struct nvval *comments;
+	struct val **comments;
 	size_t ncomments;
 	int ret;
 	int i;
@@ -79,17 +64,12 @@
 	if (!ncomments)
 		return 0;
 
-#ifdef HAVE_REALLOCARRAY
-	comments = reallocarray(NULL, ncomments, sizeof(struct nvval));
-#else
-	comments = malloc(ncomments * sizeof(struct nvval));
-#endif
+	comments = mem_reallocarray(NULL, ncomments, sizeof(struct val *));
 	if (!comments)
 		return -ENOMEM;
 
 	i = 0;
 	list_for_each(cur, list) {
-		struct nvval *comment = &comments[i++];
 		struct nvlist *c;
 
 		c = nvl_alloc();
@@ -98,8 +78,7 @@
 			goto err;
 		}
 
-		comment->type = NVT_NVL;
-		comment->nvl = c;
+		comments[i++] = nvl_cast_to_val(c);
 
 		if ((ret = nvl_set_int(c, "commid", cur->id)))
 			goto err;
@@ -117,14 +96,12 @@
 			goto err;
 	}
 
-	ret = nvl_set_array(post, "comments", comments, ncomments);
-	if (ret)
-		goto err;
-
-	return 0;
+	return nvl_set_array(post, "comments", comments, ncomments);
 
 err:
-	nvval_release_array(comments, i);
+	while (i-- > 0)
+		val_putref(comments[i]);
+
 	free(comments);
 
 	return ret;
@@ -146,15 +123,9 @@
 		 * individual post - this happens when the titlevar is not
 		 * NULL.
 		 */
-		if (post->twitter_img) {
-			struct str *tmp;
-
-			tmp = str_cat(3, str_getref(config.photo_base_url),
-				      STATIC_STR("/"),
-				      str_getref(post->twitter_img));
-
-			vars_set_str(&req->vars, "twitterimg", tmp);
-		}
+		if (post->twitter_img)
+			vars_set_str(&req->vars, "twitterimg",
+				     str_getref(post->twitter_img));
 	}
 
 	out = nvl_alloc();
@@ -209,28 +180,20 @@
 }
 
 /*
- * Fill in the `posts' array with all posts matching the prepared and bound
- * statement.
- *
- * `stmt' should be all ready to execute and it should output two columns:
- *     post id
- *     post time
+ * Set "posts", "lastupdate", and "moreposts" vars based on the array of
+ * posts passed in as @posts.
  */
 void load_posts(struct req *req, struct post **posts, int nposts,
 		bool moreposts)
 {
-	struct nvval *nvposts;
+	struct val **nvposts;
 	size_t nnvposts;
 	time_t maxtime;
 	size_t i;
 
 	maxtime = 0;
 
-#ifdef HAVE_REALLOCARRAY
-	nvposts = reallocarray(NULL, nposts, sizeof(struct nvval));
-#else
-	nvposts = malloc(nposts * sizeof(struct nvval));
-#endif
+	nvposts = mem_reallocarray(NULL, nposts, sizeof(struct val *));
 	ASSERT(nvposts);
 
 	nnvposts = 0;
@@ -240,9 +203,8 @@
 
 		post_lock(post, true);
 
-		nvposts[nnvposts].type = NVT_NVL;
-		nvposts[nnvposts].nvl = __store_vars(req, post, NULL);
-		if (IS_ERR(nvposts[nnvposts].nvl)) {
+		nvposts[nnvposts] = nvl_cast_to_val(__store_vars(req, post, NULL));
+		if (IS_ERR(nvposts[nnvposts])) {
 			post_unlock(post);
 			post_putref(post);
 			continue;
--- a/ptree.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/ptree.c	Sun Dec 30 13:23:15 2018 -0500
@@ -30,7 +30,6 @@
 
 #include "ptree.h"
 #include "utils.h"
-#include "iter.h"
 
 static struct mem_cache *ptree_cache;
 static struct mem_cache *ptnode_cache;
@@ -308,7 +307,7 @@
 		struct val *parts[3];
 		int nparts = 0; /* pacify gcc */
 
-		parts[0] = VAL_ALLOC_SYM_CSTR(pt_typename(ptn->type));
+		parts[0] = VAL_ALLOC_SYM_STATIC(pt_typename(ptn->type));
 
 		switch (ptn->type) {
 			case PT_NL:
@@ -319,16 +318,16 @@
 			case PT_STR:
 			case PT_MATH:
 			case PT_CMD:
-				parts[1] = VAL_ALLOC_STR(str_getref(ptn->u.str));
+				parts[1] = str_getref_val(ptn->u.str);
 				nparts = 2;
 				break;
 			case PT_VERB:
 				parts[1] = VAL_ALLOC_BOOL(ptn->u.verb.listing);
-				parts[2] = VAL_ALLOC_STR(str_getref(ptn->u.verb.str));
+				parts[2] = str_getref_val(ptn->u.verb.str);
 				nparts = 3;
 				break;
 			case PT_ENV:
-				parts[1] = VAL_ALLOC_STR(str_getref(ptn->u.env.name));
+				parts[1] = str_getref_val(ptn->u.env.name);
 				parts[2] = VAL_ALLOC_BOOL(ptn->u.env.begin);
 				nparts = 3;
 				break;
@@ -387,7 +386,7 @@
 			if (!args[0] || (args[0]->type != VT_STR))
 				goto err;
 
-			str = str_getref(args[0]->str);
+			str = val_getref_str(args[0]);
 
 			ptn = __ptn_new_str(type, str);
 			break;
@@ -402,7 +401,7 @@
 			if (!args[1] || (args[1]->type != VT_STR))
 				goto err;
 
-			str = str_getref(args[1]->str);
+			str = val_getref_str(args[1]);
 
 			ptn = ptn_new_verb(str, args[0]->b);
 			break;
@@ -417,7 +416,7 @@
 			if (!args[1] || (args[1]->type != VT_BOOL))
 				goto err;
 
-			str = str_getref(args[0]->str);
+			str = val_getref_str(args[0]);
 
 			ptn = ptn_new_env(args[1]->b, str);
 			break;
@@ -496,7 +495,7 @@
 		 * VT_CONS with more "stuff".
 		 */
 
-		ptn = __load_node_from_lisp(pt_nametype(str_cstr(head->str)), tail);
+		ptn = __load_node_from_lisp(pt_nametype(str_cstr(val_cast_to_str(head))), tail);
 		if (!ptn)
 			goto err;
 
--- a/render.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/render.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013-2016 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2013-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -64,10 +64,11 @@
 	struct str *raw;
 	char *out;
 
-	snprintf(path, sizeof(path), "templates/%s/%s.tmpl", req->fmt, tmpl);
+	snprintf(path, sizeof(path), "templates/%s/%s.tmpl", str_cstr(req->fmt),
+		 tmpl);
 
 	raw = file_cache_get_cb(path, revalidate_all_posts, NULL);
-	if (!raw)
+	if (IS_ERR(raw))
 		return NULL;
 
 	out = render_page(req, str_cstr(raw));
--- a/req.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/req.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2014-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -46,10 +46,14 @@
 			     str_getref(config.twitter_description));
 }
 
-void req_init(struct req *req)
+void req_init(struct req *req, struct scgi *scgi)
 {
+	set_session(scgi->id);
+
 	memset(req, 0, sizeof(struct req));
 
+	req->scgi = scgi;
+
 	/* state */
 	vars_init(&req->vars);
 	vars_set_str(&req->vars, "generatorversion", STATIC_STR(version_string));
@@ -148,7 +152,10 @@
 	nvl_set_nvl(tmp, "headers", nvl_getref(scgi->request.headers));
 	nvl_set_nvl(tmp, "query", nvl_getref(scgi->request.query));
 	nvl_set_str(tmp, "body", STR_DUP(scgi->request.body));
-	nvl_set_str(tmp, "fmt", STR_DUP(req->fmt));
+	if (req->fmt)
+		nvl_set_str(tmp, "fmt", str_getref(req->fmt));
+	else
+		nvl_set_null(tmp, "fmt");
 	nvl_set_int(tmp, "file-descriptor", scgi->fd);
 	nvl_set_int(tmp, "thread-id", (uint64_t) pthread_self());
 	nvl_set_nvl(logentry, "request", tmp);
@@ -190,7 +197,7 @@
 	nvl_set_nvl(logentry, "options", tmp);
 
 	/* serialize */
-	buf = nvl_pack(logentry, NVF_JSON);
+	buf = nvl_pack(logentry, VF_JSON);
 	if (IS_ERR(buf))
 		goto err_free;
 
@@ -218,7 +225,12 @@
 {
 	log_request(req);
 
+	str_putref(req->fmt);
+	free(req->scgi->response.body);
+
 	vars_destroy(&req->vars);
+
+	set_session(0);
 }
 
 void req_head(struct req *req, const char *name, const char *val)
@@ -229,22 +241,20 @@
 	ASSERT0(ret);
 }
 
+static const struct nvl_convert_info info[] = {
+	{ .name = "p",       .tgt_type = VT_INT, },
+	{ .name = "paged",   .tgt_type = VT_INT, },
+	{ .name = "m",       .tgt_type = VT_INT, },
+	{ .name = "preview", .tgt_type = VT_INT, },
+	{ .name = NULL, },
+};
+
 static bool select_page(struct req *req)
 {
-	struct qs *args = &req->args;
-	const struct nvpair *cur;
+	struct nvlist *query = req->scgi->request.query;
 	struct str *uri;
 
-	args->page = PAGE_INDEX;
-	args->p = -1;
-	args->paged = -1;
-	args->m = -1;
-	args->admin = 0;
-	args->comment = 0;
-	args->cat = NULL;
-	args->tag = NULL;
-	args->feed = NULL;
-	args->preview = 0;
+	req->page = PAGE_INDEX;
 
 	uri = nvl_lookup_str(req->scgi->request.headers, SCGI_DOCUMENT_URI);
 	ASSERT(!IS_ERR(uri));
@@ -252,7 +262,7 @@
 	switch (get_uri_type(uri)) {
 		case URI_STATIC:
 			/* static file */
-			args->page = PAGE_STATIC;
+			req->page = PAGE_STATIC;
 			return true;
 		case URI_DYNAMIC:
 			/* regular dynamic request */
@@ -262,57 +272,20 @@
 			return false;
 	}
 
-	nvl_for_each(cur, req->scgi->request.query) {
-		const char *name, *val;
-		const char **cptr;
-		int *iptr;
-
-		iptr = NULL;
-		cptr = NULL;
-
-		name = nvpair_name(cur);
-		val = pair2str(cur);
+	(void) nvl_convert(query, info, true);
 
-		if (!strcmp(name, "p")) {
-			iptr = &args->p;
-		} else if (!strcmp(name, "paged")) {
-			iptr = &args->paged;
-		} else if (!strcmp(name, "m")) {
-			iptr = &args->m;
-		} else if (!strcmp(name, "cat")) {
-			cptr = &args->cat;
-		} else if (!strcmp(name, "tag")) {
-			cptr = &args->tag;
-		} else if (!strcmp(name, "feed")) {
-			cptr = &args->feed;
-		} else if (!strcmp(name, "comment")) {
-			iptr = &args->comment;
-		} else if (!strcmp(name, "preview")) {
-			iptr = &args->preview;
-		} else if (!strcmp(name, "admin")) {
-			iptr = &args->admin;
-		} else {
-			return false;
-		}
-
-		if (iptr)
-			*iptr = atoi(val);
-		else if (cptr)
-			*cptr = val;
-	}
-
-	if (args->comment)
-		args->page = PAGE_COMMENT;
-	else if (args->tag)
-		args->page = PAGE_TAG;
-	else if (args->cat)
-		args->page = PAGE_CATEGORY;
-	else if (args->m != -1)
-		args->page = PAGE_ARCHIVE;
-	else if (args->p != -1)
-		args->page = PAGE_STORY;
-	else if (args->admin)
-		args->page = PAGE_ADMIN;
+	if (nvl_exists(query, "comment"))
+		req->page = PAGE_COMMENT;
+	else if (nvl_exists(query, "tag"))
+		req->page = PAGE_TAG;
+	else if (nvl_exists(query, "cat"))
+		req->page = PAGE_CATEGORY;
+	else if (nvl_exists(query, "m"))
+		req->page = PAGE_ARCHIVE;
+	else if (nvl_exists(query, "p"))
+		req->page = PAGE_STORY;
+	else if (nvl_exists(query, "admin"))
+		req->page = PAGE_ADMIN;
 
 	return true;
 }
@@ -329,42 +302,52 @@
  */
 static bool switch_content_type(struct req *req)
 {
-	const char *fmt = req->args.feed;
-	int page = req->args.page;
+	struct str *fmt;
 
 	const char *content_type;
 	int index_stories;
 
+	fmt = nvl_lookup_str(req->scgi->request.query, "feed");
+	if (IS_ERR(fmt)) {
+		if (PTR_ERR(fmt) != -ENOENT)
+			return false; /* internal error */
+
+		fmt = NULL;
+	}
+
 	if (!fmt) {
 		/* no feed => OK, use html */
-		fmt = "html";
+		fmt = STATIC_STR("html");
 		content_type = "text/html";
 		index_stories = config.html_index_stories;
-	} else if (!strcmp(fmt, "atom")) {
+	} else if (!strcmp(str_cstr(fmt), "atom")) {
 		content_type = "application/atom+xml";
 		index_stories = config.feed_index_stories;
-	} else if (!strcmp(fmt, "rss2")) {
+	} else if (!strcmp(str_cstr(fmt), "rss2")) {
 		content_type = "application/rss+xml";
 		index_stories = config.feed_index_stories;
 	} else {
 		/* unsupported feed type */
+		str_putref(fmt);
 		return false;
 	}
 
-	switch (page) {
+	switch (req->page) {
 		case PAGE_INDEX:
 		case PAGE_STORY:
 			break;
 
 		/* for everything else, we have only HTML */
 		default:
-			if (strcmp(fmt, "html"))
+			if (strcmp(str_cstr(fmt), "html")) {
+				str_putref(fmt);
 				return false;
+			}
 			break;
 	}
 
 	/* let the template engine know */
-	req->fmt = fmt;
+	req->fmt = fmt; /* pass along the reference gotten from lookup */
 
 	/* let the client know */
 	req_head(req, "Content-Type", content_type);
@@ -375,6 +358,25 @@
 	return true;
 }
 
+/*
+ * Get the request page number.
+ *
+ * If we got a reasonable looking page number, let's use it.  Otherwise, we
+ * use the first page - page 0.
+ */
+static int get_page_number(struct req *req)
+{
+	uint64_t tmp;
+
+	if (nvl_lookup_int(req->scgi->request.query, "paged", &tmp))
+		return 0;
+
+	if (tmp > INT_MAX)
+		return 0;
+
+	return tmp;
+}
+
 int req_dispatch(struct req *req)
 {
 	if (!select_page(req))
@@ -386,25 +388,21 @@
 	if (!switch_content_type(req))
 		return R404(req, "{error_unsupported_feed_fmt}");
 
-	switch (req->args.page) {
+	switch (req->page) {
 		case PAGE_STATIC:
 			return blahg_static(req);
 		case PAGE_ARCHIVE:
-			return blahg_archive(req, req->args.m,
-					     req->args.paged);
+			return blahg_archive(req, get_page_number(req));
 		case PAGE_CATEGORY:
-			return blahg_category(req, req->args.cat,
-					      req->args.paged);
+			return blahg_category(req, get_page_number(req));
 		case PAGE_TAG:
-			return blahg_tag(req, req->args.tag,
-					 req->args.paged);
+			return blahg_tag(req, get_page_number(req));
 		case PAGE_COMMENT:
 			return blahg_comment(req);
 		case PAGE_INDEX:
-			return blahg_index(req, req->args.paged);
+			return blahg_index(req, get_page_number(req));
 		case PAGE_STORY:
-			return blahg_story(req, req->args.p,
-					   req->args.preview == PREVIEW_SECRET);
+			return blahg_story(req);
 		case PAGE_ADMIN:
 			return blahg_admin(req);
 		default:
@@ -415,12 +413,14 @@
 
 int R404(struct req *req, char *tmpl)
 {
+	str_putref(req->fmt);
+
 	tmpl = tmpl ? tmpl : "{404}";
 
 	req_head(req, "Content-Type", "text/html");
 
 	req->scgi->response.status = SCGI_STATUS_NOTFOUND;
-	req->fmt = "html";
+	req->fmt = STATIC_STR("html");
 
 	vars_scope_push(&req->vars);
 
@@ -433,13 +433,15 @@
 
 int R301(struct req *req, const char *url)
 {
+	str_putref(req->fmt);
+
 	DBG("status 301 (url: '%s')", url);
 
 	req_head(req, "Content-Type", "text/html");
 	req_head(req, "Location", url);
 
 	req->scgi->response.status = SCGI_STATUS_REDIRECT;
-	req->fmt = "html";
+	req->fmt = STATIC_STR("html");
 
 	vars_scope_push(&req->vars);
 
--- a/req.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/req.h	Sun Dec 30 13:23:15 2018 -0500
@@ -30,7 +30,7 @@
 
 #include "vars.h"
 
-enum {
+enum page {
 	PAGE_ARCHIVE,
 	PAGE_CATEGORY,
 	PAGE_TAG,
@@ -41,37 +41,23 @@
 	PAGE_STATIC,
 };
 
-struct qs {
-	int page;
-
-	int p;
-	int paged;
-	int m;
-	int preview;
-	int admin;
-	int comment;
-	const char *cat;
-	const char *tag;
-	const char *feed;
-};
-
 struct req {
 	struct scgi *scgi;
 
 	/* request */
-	struct qs args;
+	enum page page;
 
 	/* state */
 	struct vars vars;
 
-	const char *fmt;	/* format (e.g., "html") */
+	struct str *fmt;	/* format (e.g., "html") */
 
 	struct {
 		int index_stories;
 	} opts;
 };
 
-extern void req_init(struct req *req);
+extern void req_init(struct req *req, struct scgi *scgi);
 extern void req_destroy(struct req *req);
 extern void req_output(struct req *req);
 extern void req_head(struct req *req, const char *name, const char *val);
@@ -80,12 +66,12 @@
 extern int R404(struct req *req, char *tmpl);
 extern int R301(struct req *req, const char *url);
 
-extern int blahg_archive(struct req *req, int m, int paged);
-extern int blahg_category(struct req *req, const char *cat, int page);
-extern int blahg_tag(struct req *req, const char *tag, int paged);
+extern int blahg_archive(struct req *req, int paged);
+extern int blahg_category(struct req *req, int page);
+extern int blahg_tag(struct req *req, int paged);
 extern int blahg_comment(struct req *req);
 extern int blahg_index(struct req *req, int paged);
-extern int blahg_story(struct req *req, int p, bool preview);
+extern int blahg_story(struct req *req);
 extern int blahg_admin(struct req *req);
 
 extern int init_wordpress_categories(void);
--- a/sidebar.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/sidebar.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2013-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,7 @@
 #include <sys/sysmacros.h>
 
 #include <jeffpc/error.h>
+#include <jeffpc/mem.h>
 
 #include "req.h"
 #include "vars.h"
@@ -35,7 +36,7 @@
 
 struct tagcloud_state {
 	unsigned long ntags;
-	struct nvval *cloud;
+	struct val **cloud;
 };
 
 static int __tag_size(int count, int cmin, int cmax)
@@ -56,11 +57,7 @@
 {
 	struct tagcloud_state *state = arg;
 
-#ifdef HAVE_REALLOCARRAY
-	state->cloud = reallocarray(NULL, ntags, sizeof(struct nvval));
-#else
-	state->cloud = malloc(ntags * sizeof(struct nvval));
-#endif
+	state->cloud = mem_reallocarray(NULL, ntags, sizeof(struct val *));
 	state->ntags = 0;
 
 	return state->cloud ? 0 : -ENOMEM;
@@ -90,8 +87,7 @@
 	if ((ret = nvl_set_int(tmp, "size", __tag_size(count, cmin, cmax))))
 		goto err;
 
-	state->cloud[state->ntags].type = NVT_NVL;
-	state->cloud[state->ntags].nvl = tmp;
+	state->cloud[state->ntags] = nvl_cast_to_val(tmp);
 	state->ntags++;
 
 	return;
--- a/static.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/static.h	Sun Dec 30 13:23:15 2018 -0500
@@ -23,7 +23,7 @@
 #ifndef __STATIC_H
 #define __STATIC_H
 
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 
 #include "req.h"
 
--- a/story.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/story.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@
 static int __load_post(struct req *req, int p, bool preview)
 {
 	struct nvlist *post;
-	struct nvval *val;
+	struct val **val;
 
 	post = get_post(req, p, "title", preview);
 	if (!post) {
@@ -49,30 +49,52 @@
 		return -ENOENT;
 	}
 
-	val = malloc(sizeof(struct nvval));
-	if (!val)
+	val = malloc(sizeof(struct val *));
+	if (!val) {
+		DBG("failed to allocate val array");
+
+		nvl_putref(post);
+
 		return -ENOMEM;
+	}
 
-	val->type = NVT_NVL;
-	val->nvl = post;
+	*val = nvl_cast_to_val(post);
 
 	vars_set_array(&req->vars, "posts", val, 1);
 
+	if (preview)
+		vars_set_int(&req->vars, "preview", 1);
+
 	return 0;
 }
 
-int blahg_story(struct req *req, int p, bool preview)
+/*
+ * Is the request a preview?
+ */
+static bool is_preview(struct req *req)
 {
-	if (p == -1) {
-		DBG("Invalid post #");
-		return 0;
-	}
+	uint64_t tmp;
+
+	if (nvl_lookup_int(req->scgi->request.query, "preview", &tmp))
+		return false;
+
+	return tmp == PREVIEW_SECRET;
+}
+
+int blahg_story(struct req *req)
+{
+	uint64_t postid;
+
+	if (nvl_lookup_int(req->scgi->request.query, "p", &postid))
+		return R404(req, NULL);
+	if (postid > INT_MAX)
+		return R404(req, NULL);
 
 	sidebar(req);
 
 	vars_scope_push(&req->vars);
 
-	if (__load_post(req, p, preview))
+	if (__load_post(req, postid, is_preview(req)))
 		return R404(req, NULL);
 
 	req->scgi->response.body = render_page(req, "{storyview}");
--- a/tag.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/tag.c	Sun Dec 30 13:23:15 2018 -0500
@@ -47,19 +47,20 @@
  */
 static struct str **wordpress_cats;
 
-static void __store_title(struct vars *vars, const char *title)
+static void __store_title(struct vars *vars, struct str *title)
 {
 	char twittertitle[1024];
 
-	snprintf(twittertitle, sizeof(twittertitle), "%s » %s", "Blahg", title);
+	snprintf(twittertitle, sizeof(twittertitle), "%s » %s", "Blahg",
+		 str_cstr(title));
 
-	vars_set_str(vars, "title", STR_DUP(title));
+	vars_set_str(vars, "title", title);
 	vars_set_str(vars, "twittertitle", STR_DUP(twittertitle));
 }
 
-static void __store_tag(struct vars *vars, const char *tag)
+static void __store_tag(struct vars *vars, struct str *tag)
 {
-	vars_set_str(vars, "tagid", STR_DUP(tag));
+	vars_set_str(vars, "tagid", tag);
 }
 
 static void __store_pages(struct vars *vars, int page)
@@ -69,26 +70,21 @@
 	vars_set_int(vars, "nextpage", page - 1);
 }
 
-int __tagcat(struct req *req, const char *tagcat, int page, char *tmpl,
+int __tagcat(struct req *req, struct str *tag, int page, char *tmpl,
 	     bool istag)
 {
 	const unsigned int posts_per_page = req->opts.index_stories;
 	struct post *posts[posts_per_page];
-	struct str *tag;
 	int nposts;
 
-	if (!tagcat)
+	if (IS_ERR(tag))
 		return R404(req, NULL);
 
-	tag = STR_DUP(tagcat);
-
 	req_head(req, "Content-Type", "text/html");
 
-	page = MAX(page, 0);
-
-	__store_title(&req->vars, tagcat);
+	__store_title(&req->vars, str_getref(tag));
 	__store_pages(&req->vars, page);
-	__store_tag(&req->vars, tagcat);
+	__store_tag(&req->vars, str_getref(tag));
 
 	sidebar(req);
 
@@ -106,21 +102,27 @@
 	return 0;
 }
 
-int blahg_tag(struct req *req, const char *tag, int page)
+int blahg_tag(struct req *req, int page)
 {
-	return __tagcat(req, tag, page, "{tagindex}", true);
+	return __tagcat(req, nvl_lookup_str(req->scgi->request.query, "tag"),
+			page, "{tagindex}", true);
 }
 
-int blahg_category(struct req *req, const char *cat, int page)
+int blahg_category(struct req *req, int page)
 {
+	struct str *cat;
 	uint32_t catn;
 
+	cat = nvl_lookup_str(req->scgi->request.query, "cat");
+	if (IS_ERR(cat))
+		goto out;
+
 	/*
 	 * If we fail to parse the category number or it refers to a
 	 * non-mapped category, we just use it as is.
 	 */
 
-	if (wordpress_cats && !str2u32(cat, &catn)) {
+	if (wordpress_cats && !str2u32(str_cstr(cat), &catn)) {
 		char url[256];
 
 		if (catn >= array_size(wordpress_cats))
@@ -129,6 +131,8 @@
 		if (!wordpress_cats[catn])
 			goto out;
 
+		str_putref(cat);
+
 		snprintf(url, sizeof(url), "%s/?cat=%s",
 			 str_cstr(config.base_url),
 			 str_cstr(wordpress_cats[catn]));
@@ -148,11 +152,10 @@
 	struct val *idx, *name;
 	int ret;
 
-	if (!cur)
-		return 0; /* empty list is ok */
-
-	if (cur->type != VT_CONS)
+	if (cur->type != VT_CONS) {
+		val_putref(cur);
 		return -EINVAL;
+	}
 
 	idx = sexpr_car(val_getref(cur));
 	name = sexpr_cdr(cur);
@@ -170,7 +173,7 @@
 			goto out;
 	}
 
-	wordpress_cats[idx->i] = str_getref(name->str);
+	wordpress_cats[idx->i] = val_getref_str(name);
 
 	ret = 0;
 
@@ -188,7 +191,7 @@
 
 	wordpress_cats = array_alloc(sizeof(struct str *), 0);
 
-	sexpr_for_each(cur, tmp, config.wordpress_categories) {
+	sexpr_for_each_noref(cur, tmp, config.wordpress_categories) {
 		ret = store_wordpress_category(val_getref(cur));
 		if (ret)
 			break;
--- a/template.y	Sun Aug 06 15:48:33 2017 +0300
+++ b/template.y	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2012-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -34,7 +34,6 @@
 #include <jeffpc/val.h>
 
 #include "config.h"
-#include "iter.h"
 #include "vars.h"
 #include "render.h"
 #include "pipeline.h"
@@ -77,7 +76,7 @@
 
 static char *__foreach(struct req *req, const struct nvpair *var, char *tmpl)
 {
-	const struct nvval *items;
+	struct val **items;
 	size_t nitems;
 	size_t i;
 	char *out;
@@ -91,13 +90,14 @@
 	for (i = 0; i < nitems; i++) {
 		vars_scope_push(&req->vars);
 
-		switch (items[i].type) {
-			case NVT_NVL:
-				vars_merge(&req->vars, items[i].nvl);
+		switch (items[i]->type) {
+			case VT_NVL:
+				vars_merge(&req->vars,
+					   val_cast_to_nvl(items[i]));
 				break;
-			case NVT_STR:
+			case VT_STR:
 				vars_set_str(&req->vars, nvpair_name(var),
-					     str_getref(items[i].str));
+					     val_getref_str(items[i]));
 				break;
 			default:
 				//vars_dump(&req->vars);
@@ -138,16 +138,20 @@
 
 	switch (val->type) {
 		case VT_STR:
-			tmp = str_cstr(val->str);
+			tmp = str_cstr(val_cast_to_str(val));
 			break;
 		case VT_INT:
 			snprintf(buf, sizeof(buf), "%"PRIu64, val->i);
 			tmp = buf;
 			break;
+		case VT_BLOB:
+		case VT_NULL:
 		case VT_SYM:
 		case VT_CONS:
 		case VT_BOOL:
 		case VT_CHAR:
+		case VT_ARRAY:
+		case VT_NVL:
 			panic("%s called with value of type %d", __func__,
 			      val->type);
 	}
@@ -162,12 +166,12 @@
 	char *ret;
 
 	switch (nvpair_type(var)) {
-		case NVT_STR:
+		case VT_STR:
 			str = nvpair_value_str(var);
 			ret = xstrdup(str_cstr(str));
 			str_putref(str);
 			break;
-		case NVT_INT:
+		case VT_INT:
 			snprintf(buf, sizeof(buf), "%"PRIu64, pair2int(var));
 			ret = xstrdup(buf);
 			break;
@@ -200,10 +204,10 @@
 	}
 
 	switch (nvpair_type(var)) {
-		case NVT_STR:
-			val = VAL_ALLOC_STR(nvpair_value_str(var));
+		case VT_STR:
+			val = str_cast_to_val(nvpair_value_str(var));
 			break;
-		case NVT_INT:
+		case VT_INT:
 			val = VAL_ALLOC_INT(pair2int(var));
 			break;
 		default:
@@ -259,7 +263,7 @@
 		return 0;
 
 	switch (nvpair_type(var)) {
-		case NVT_INT:
+		case VT_INT:
 			return pair2int(var);
 		default:
 			panic("unexpected nvpair type: %d",
--- a/templates/html/story.tmpl	Sun Aug 06 15:48:33 2017 +0300
+++ b/templates/html/story.tmpl	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 <div class="story">
-	<h2 class="storytitle"><a href="?p={id}" rel="bookmark">{title|escape}</a></h2>
+	<h2 class="storytitle">{ifset(preview)}Preview: {endif()}<a href="?p={id}" rel="bookmark">{title|escape}</a></h2>
 	<div class="storymeta">
 		Filed under:
 		<ul>
@@ -8,6 +8,25 @@
 		&#8212; JeffPC @ <span title="{time|zulu}">{time|date} {time|time}</span>
 	</div>
 
+{ifset(preview)}
+	<br/>
+	<div style="border: 3px solid red; width: 90%">
+{ifset(twitteruser)}
+{ifset(twittertitle)}
+{ifset(twitterdesc)}
+{ifset(twitterimg)}
+	<div>
+	<img src="{twitterimg}" width="100%" />
+	</div>
+{endif()}
+	<strong>{twittertitle|escape}</strong><br/>
+	<small>{twitterdesc|escape}</small>
+{endif()}
+{endif()}
+{endif()}
+	</div>
+{endif()}
+
 	<div class="storycontent">
 {body}
 	</div>
--- a/test_fmt3.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/test_fmt3.c	Sun Dec 30 13:23:15 2018 -0500
@@ -24,7 +24,6 @@
 
 #include <jeffpc/jeffpc.h>
 #include <jeffpc/error.h>
-#include <jeffpc/str.h>
 #include <jeffpc/val.h>
 
 #include "parse.h"
--- a/utils.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/utils.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2016 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2011-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -153,20 +153,3 @@
 
 	return mktime(&tm);
 }
-
-/*
- * libavl extensions
- */
-
-/* like avl_add, but returns the existing node */
-void *safe_avl_add(avl_tree_t *tree, void *node)
-{
-	avl_index_t where;
-	void *tmp;
-
-	tmp = avl_find(tree, node, &where);
-	if (!tmp)
-		avl_insert(tree, node, where);
-
-	return tmp;
-}
--- a/utils.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/utils.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2011-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +26,13 @@
 #include <sys/sysmacros.h>
 #include <sys/stat.h>
 #include <string.h>
-#include <sys/avl.h>
 
 #include <jeffpc/error.h>
 #include <jeffpc/int.h>
-#include <jeffpc/str.h>
+#include <jeffpc/val.h>
 #include <jeffpc/io.h>
 #include <jeffpc/time.h>
+#include <jeffpc/rbtree.h>
 
 extern int hasdotdot(const char *path);
 extern char *concat5(char *a, char *b, char *c, char *d, char *e);
@@ -72,10 +72,4 @@
 	return ret;
 }
 
-/*
- * libavl extensions
- */
-
-extern void *safe_avl_add(avl_tree_t *tree, void *node);
-
 #endif
--- a/vars.c	Sun Aug 06 15:48:33 2017 +0300
+++ b/vars.c	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2013-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -95,7 +95,7 @@
 
 WRAP_SET1(vars_set_str, nvl_set_str, struct str *);
 WRAP_SET1(vars_set_int, nvl_set_int, uint64_t);
-WRAP_SET2(vars_set_array, nvl_set_array, struct nvval *);
+WRAP_SET2(vars_set_array, nvl_set_array, struct val **);
 
 const struct nvpair *vars_lookup(struct vars *vars, const char *name)
 {
--- a/vars.h	Sun Aug 06 15:48:33 2017 +0300
+++ b/vars.h	Sun Dec 30 13:23:15 2018 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ * Copyright (c) 2013-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -38,7 +38,7 @@
 extern void vars_set_str(struct vars *vars, const char *name, struct str *val);
 extern void vars_set_int(struct vars *vars, const char *name, uint64_t val);
 extern void vars_set_array(struct vars *vars, const char *name,
-		struct nvval *val, size_t nval);
+		struct val **vals, size_t nval);
 extern const struct nvpair *vars_lookup(struct vars *vars, const char *name);
 extern struct str *vars_lookup_str(struct vars *vars, const char *name);
 extern uint64_t vars_lookup_int(struct vars *vars, const char *name);