summary refs log tree commit diff
diff options
context:
space:
mode:
authorWormHeamer2025-03-08 16:45:17 -0500
committerWormHeamer2025-03-08 16:45:17 -0500
commit4af753d591e61a7380735e03a8658fb8949e0448 (patch)
treeae92949580512dadecd26b8c3a359f2bd7755d58
initial commit
-rw-r--r--Makefile30
-rw-r--r--arena.h56
-rw-r--r--args.h138
-rw-r--r--main.c409
-rw-r--r--str.h102
-rw-r--r--typ.h7
6 files changed, 742 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..50fd1dc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+EXE = wdoc
+RUNARGS =
+
+CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -Os ${CFLAGS_${DEBUG}}
+LDFLAGS = -flto ${LDFLAGS_${DEBUG}}
+PREFIX = /usr/local/
+LDLIBS =
+
+DEBUG = 0
+GDB != which gf2 2> /dev/null || which gdb
+
+CFLAGS_1 = -g -fsanitize=undefined
+LDFLAGS_1 = -g -fsanitize=undefined
+LDFLAGS_0 = -s
+
+OBJ != find -type f -name '*.c' | sed 's/\.c$$/.o/'
+
+.PHONY: run all clean
+
+all: ${EXE}
+run: ${EXE}
+	./${EXE} ${RUNARGS}
+debug: ${EXE}
+	${GDB} -ex start --args ./${EXE} ${RUNARGS}
+
+clean:
+	rm -fv ${EXE} ${OBJ}
+
+${EXE}: ${OBJ}
+	${CC} ${LDFLAGS} ${OBJ} -o ${EXE} ${LDLIBS}
diff --git a/arena.h b/arena.h
new file mode 100644
index 0000000..2058559
--- /dev/null
+++ b/arena.h
@@ -0,0 +1,56 @@
+#ifndef ARENA_H
+#define ARENA_H
+
+#include "typ.h"
+
+typedef struct {
+	char *beg, *end;
+} Arena;
+
+#define new(a, t)\
+	zeroed(alloc(a, sizeof(t), _Alignof(t)), sizeof(t))
+
+#define new_arr(a, t, n)\
+	alloc(a, sizeof(t) * n, _Alignof(t))
+
+#define resize(a, p, old, new)\
+	re_alloc(a, p, (old) * sizeof(*(p)), (new) * sizeof(*(p)),\
+			_Alignof(__typeof__(*(p))))
+
+void *alloc(Arena *a, isize n, isize align);
+void *re_alloc(Arena *a, void *ptr, isize old, isize new, isize align);
+void *zeroed(void *p, usize n);
+
+#ifdef ARENA_IMPL
+
+#include <stdio.h>
+#include <stdlib.h>
+
+void *alloc(Arena *a, isize n, isize align) {
+	char *p = a->beg + (-(uintptr_t)a->beg & (align - 1));
+	if (p + n >= a->end) {
+		fprintf(stderr, "out of arena memory!\n");
+		abort();
+	}
+	a->beg = p + n;
+	return p;
+}
+
+void *zeroed(void *p, usize n) {
+	memset(p, 0, n);
+	return p;
+}
+
+void *re_alloc(Arena *a, void *ptr, isize old, isize new, isize align) {
+	if (ptr && a->beg - old == ptr && a->beg - old + new < a->end) {
+		a->beg = a->beg - old + new;
+		return ptr;
+	} else {
+		void *p = alloc(a, new, align);
+		if (ptr) memcpy(p, ptr, old);
+		return p;
+	}
+}
+
+#endif
+#endif
diff --git a/args.h b/args.h
new file mode 100644
index 0000000..35c5283
--- /dev/null
+++ b/args.h
@@ -0,0 +1,138 @@
+#ifndef ARGS_H
+#define ARGS_H
+
+#include "str.h"
+
+typedef struct {
+	const char **arg;
+	const char *opt_end;
+} ArgsState;
+
+typedef enum {
+	ARG_OK = 0,
+	ARG_END = -1,
+	ARG_BAD = -2,
+	ARG_EMPTY = -3
+} ArgResult;
+
+ArgsState args_begin(const char **argv);
+ArgResult arg_getv(ArgsState *a, const char *fmt, Str *arg, ...);
+#define arg_get(a, fmt, arg, ...)\
+	    arg_getv(a, fmt, arg __VA_OPT__(,) __VA_ARGS__, NULL)
+
+#ifdef ARGS_IMPL
+#include <stdarg.h>
+
+ArgsState args_begin(const char **argv) {
+	return (ArgsState) { argv + 1, NULL };
+}
+
+static int arg_opt_find(const char **opts, Str key) {
+	for (int i = 0; opts[i]; i++) {
+		const char *o = opts[i];
+		if (*o == ':') o++;
+		if (str_eql(str_from_cstr(o), key)) return i;
+	}
+	return -1;
+}
+
+static ArgResult arg_param(ArgsState *a, Str name, Str rem, Str *arg) {
+	if (rem.n > 0) {
+		*arg = rem;
+		return ARG_OK;
+	} else if (a->arg[1]) {
+		*arg = str_from_cstr(*++a->arg);
+		return ARG_OK;
+	} else {
+		*arg = name;
+		return ARG_EMPTY;
+	}
+}
+
+static int arg_got_long(ArgsState *a, const char **opts, int *optv, Str *arg) {
+	Cut key = str_cut(str_from_cstr(*a->arg + 2), '=');
+	if (opts && optv) {
+		int o = arg_opt_find(opts, key.head);
+		if (o < 0) {
+			*arg = key.head;
+			return ARG_BAD;
+		}
+		if (opts[o][0] == ':') {
+			int x = arg_param(a, key.head, key.tail, arg);
+			if (x < 0) return x;
+		}
+		a->arg++;
+		return optv[o];
+	}
+	*arg = key.head;
+	return ARG_BAD;
+}
+
+static ArgResult arg_got_short(ArgsState *a, const char *fmt, Str *arg) {
+	Str opt = { (char*)*a->arg, 1 };
+	Str rem = str_from_cstr(*a->arg + 1);
+	for (const char *f = fmt; *f; f++) {
+		if (*f == ':') continue;
+		if (*f == **a->arg) {
+			if (f[1] == ':') {
+				int x = arg_param(a, opt, rem, arg);
+				if (x < 0) return x;
+				a->arg++;
+			} else {
+				(*a->arg)++;
+			}
+			return *f;
+		}
+	}
+	*arg = opt;
+	return ARG_BAD;
+}
+
+static ArgResult arg_get_long(ArgsState *a, const char *fmt, const char **opts, int *optv, Str *arg) {
+	if (*a->arg && !**a->arg) a->arg++;
+	if (!*a->arg) return ARG_END;
+	const char *arg_end = *a->arg + strlen(*a->arg);
+	if (a->opt_end != arg_end) {
+		if (a->arg[0][0] != '-' || a->arg[0][1] == '\0') {
+			*arg = str_from_cstr(*a->arg++);
+			return ARG_OK;
+		}
+		if (a->arg[0][1] == '-') {
+			return arg_got_long(a, opts, optv, arg);
+		}
+		(*a->arg)++;
+		a->opt_end = arg_end;
+	}
+	return arg_got_short(a, fmt, arg);
+}
+
+ArgResult arg_getv(ArgsState *a, const char *fmt, Str *arg, ...) {
+	/* I think this is a legitimate usecase for VLAs --- they're not
+	 * safe if N depends on user input, but here it very much doesn't!
+	 * Just on the number of arguments passed, which is a compile time
+	 * constant. */
+	va_list ap;
+	int n = 0;
+	va_start(ap, arg);
+	while (va_arg(ap, const char *)) {
+		n++;
+		(void)va_arg(ap, int);
+	}
+	va_end(ap);
+	if (n > 0) {
+		const char *opt[n];
+		int optv[n];
+		va_start(ap, arg);
+		for (int i = 0; i < n; i++) {
+			opt[i] = va_arg(ap, const char *);
+			optv[i] = va_arg(ap, int);
+		}
+		va_end(ap);
+		return arg_get_long(a, fmt, opt, optv, arg);
+	} else {
+		return arg_get_long(a, fmt, NULL, NULL, arg);
+	}
+}
+
+#endif
+#endif
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..8dc8a6e
--- /dev/null
+++ b/main.c
@@ -0,0 +1,409 @@
+#define _POSIX_C_SOURCE 200809L
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#define ARENA_IMPL
+#include "str.h"
+#include "arena.h"
+#define ARGS_IMPL
+#include "args.h"
+
+int read_all(FILE *f, Str *buf, Arena *a) {
+	if (!f) return -1;
+	if (isatty(fileno(f))) {
+		buf->s = a->beg;
+		buf->n = fread(a->beg, 1, (uintptr_t)(a->end - a->beg) - 1, f);
+		a->beg += buf->n;
+	} else {
+		fseek(f, 0, SEEK_END);
+		long ofs = ftell(f);
+		fseek(f, 0, SEEK_SET);
+		buf->n = ofs;
+		buf->s = new_arr(a, char, buf->n);
+		if ((isize)fread(buf->s, 1, buf->n, f) != buf->n) return -1;
+	}
+	return ferror(f) ? -1 : 0;
+}
+
+int next_line(Str *src, Str *line) {
+	if (src->n < 1) return 0;
+	line->s = src->s;
+	char *newln = memchr(src->s, '\n', src->n);
+	line->n = newln ? newln - src->s : src->n;
+	src->s += line->n + 1;
+	src->n -= line->n + 1;
+	return 1;
+}
+
+void str_putf(Str s, FILE *f) {
+	fwrite(s.s, 1, s.n, f);
+}
+
+void str_put(Str s) {
+	str_putf(s, stdout);
+}
+
+char to_xdigit(int x) {
+	if (x > 9) {
+		return 'A' + (x - 10);
+	} else {
+		return '0' + x;
+	}
+}
+
+void str_cat_uri(Str *s, Str uri, Arena *a) {
+	str_catc(s, '\'', a);
+	for (isize i = 0; i < uri.n; i++) {
+		char c = uri.s[i];
+		if (c == '\'' || c == '%') {
+			str_catc(s, '%', a);
+			str_catc(s, to_xdigit((c & 0xff) >> 4), a);
+			str_catc(s, to_xdigit(c & 0xf), a);
+		} else {
+			str_catc(s, c, a);
+		}
+	}
+	str_catc(s, '\'', a);
+}
+
+void str_cat_html(Str *s, Str uri, Arena *a) {
+	for (isize i = 0; i < uri.n; i++) {
+		char c = uri.s[i];
+		switch (c) {
+		case '&': str_cat(s, S("&amp;"), a); break;
+		case '<': str_cat(s, S("&lt;"), a); break;
+		case '>': str_cat(s, S("&gt;"), a); break;
+		default: str_catc(s, c, a); break;
+		}
+	}
+}
+
+int is_ol_item(Str s) {
+	Str h = str_cut(s, '.').head;
+	if (h.n < 1) return 0;
+	for (isize i = 0; i < h.n; i++) {
+		if (!(h.s[i] >= '0' && h.s[i] <= '9')) return 0;
+	}
+	return 1;
+}
+
+typedef enum {
+	LINE_BLANK, LINE_PARA,
+	LINE_LINK, LINE_FIGURE,
+	LINE_UL, LINE_OL,
+	LINE_HDR1, LINE_HDR2, LINE_HDR3, LINE_CODE,
+	LINE_BQUOT,
+} LineMode;
+
+LineMode lm_chg(LineMode from, LineMode to, Str *out, Arena *a) {
+	static Str op[] = {
+		[LINE_BLANK] = S(""),
+		[LINE_PARA] = S("<p>"),
+		[LINE_LINK] = S("<ul>\n<li>"),
+		[LINE_FIGURE] = S("<figure>"),
+		[LINE_UL] = S("<ul>\n<li>"),
+		[LINE_OL] = S("<ol>\n<li>"),
+		[LINE_HDR1] = S("<h1>"),
+		[LINE_HDR2] = S("<h2>"),
+		[LINE_HDR3] = S("<h3>"),
+		[LINE_CODE] = S("<pre><code>"),
+		[LINE_BQUOT] = S("<blockquote>"),
+	};
+	static Str cl[] = {
+		[LINE_BLANK] = S(""),
+		[LINE_PARA] = S("</p>"),
+		[LINE_LINK] = S("</li>\n</ul>"),
+		[LINE_FIGURE] = S("</figure>"),
+		[LINE_UL] = S("</li>\n</ul>"),
+		[LINE_OL] = S("</li>\n</ol>"),
+		[LINE_HDR1] = S("</h1>"),
+		[LINE_HDR2] = S("</h2>"),
+		[LINE_HDR3] = S("</h3>"),
+		[LINE_CODE] = S("</code></pre>"),
+		[LINE_BQUOT] = S("</blockquote>"),
+	};
+	static Str cont[] = {
+		[LINE_BLANK] = S(""),
+		[LINE_PARA] = S("<br>\n"),
+		[LINE_FIGURE] = S("</figure>\n<figure>"),
+		[LINE_LINK] = S("</li>\n<li>"),
+		[LINE_UL] = S("</li>\n<li>"),
+		[LINE_OL] = S("</li>\n<li>"),
+		[LINE_HDR1] = S("</h1>\n<h1>"),
+		[LINE_HDR2] = S("</h2>\n<h2>"),
+		[LINE_HDR3] = S("</h3>\n<h3>"),
+		[LINE_CODE] = S("\n"),
+		[LINE_BQUOT] = S("<br>\n"),
+	};
+	if (from == to) {
+		str_cat(out, cont[from], a);
+	} else {
+		str_cat(out, cl[from], a);
+		str_catc(out, '\n', a);
+		str_cat(out, op[to], a);
+	}
+	return to;
+}
+
+typedef struct Doc Doc;
+struct Doc {
+	Str html;
+	Str title;
+	Doc *prev, *next;
+};
+
+int has_image_ext(Str url) {
+	return str_ends(url, S(".png"))
+		|| str_ends(url, S(".jpg"))
+		|| str_ends(url, S(".jpeg"))
+		|| str_ends(url, S(".webp"));
+}
+
+Str str_replace_end(Str s, Str a, Str b, Arena *m) {
+	if (!str_ends(s, a)) return s;
+	char *p = new_arr(m, char, s.n + b.n - a.n);
+	memcpy(p, s.s, s.n - a.n);
+	memcpy(p + s.n - a.n, b.s, b.n);
+	return (Str) { p, s.n + b.n - a.n };
+}
+
+int wdoc(FILE *f, Doc **dp, Arena *a, Arena *scratch) {
+	Str buf, line, out = {0}, title = {0};
+	if (read_all(f, &buf, scratch)) return -1;
+	LineMode lm = LINE_BLANK;
+	while (next_line(&buf, &line)) {
+		if (str_starts(line, S("```"))) {
+			lm = lm_chg(lm, lm == LINE_CODE ? LINE_BLANK : LINE_CODE, &out, a);
+			continue;
+		} else if (lm == LINE_CODE) {
+			lm = lm_chg(lm, LINE_CODE, &out, a);
+			str_cat(&out, line, a);
+			continue;
+		} else if (line.n == 0) {
+			lm = lm_chg(lm, LINE_BLANK, &out, a);
+		} else if (str_starts(line, S("=>"))) {
+			line = str_trim(str_skip(line, 2));
+			isize i = 0;
+			while (i < line.n && !is_space(line.s[i])) i++;
+			Str url = { line.s, i };
+			line = str_trim(str_skip(line, i));
+			if (!str_starts(url, S("gemini://"))) {
+				url = str_replace_end(url, S(".gmi"), S(".html"), scratch);
+			}
+			if (has_image_ext(url)) {
+				lm = lm_chg(lm, LINE_FIGURE, &out, a);
+				str_cat(&out, S("<img src="), a);
+				str_cat_uri(&out, url, a);
+				str_catc(&out, '>', a);
+				if (line.n > 0) {
+					str_cat(&out, S("<figcaption>"), a);
+					str_cat_html(&out, line, a);
+					str_cat(&out, S("</figcaption>"), a);
+				}
+			} else {
+				Str display = line.n > 0 ? line : url;
+				lm = lm_chg(lm, LINE_LINK, &out, a);
+				str_cat(&out, S("<a href="), a);
+				str_cat_uri(&out, url, a);
+				str_catc(&out, '>', a);
+				str_cat_html(&out, display, a);
+				str_cat(&out, S("</a>"), a);
+			}
+		} else if (str_starts(line, S("*"))) {
+			lm = lm_chg(lm, LINE_UL, &out, a);
+			str_cat_html(&out, str_trim(str_skip(line, 1)), a);
+		} else if (is_ol_item(line)) {
+			lm = lm_chg(lm, LINE_OL, &out, a);
+			str_cat_html(&out, str_trim(str_cut(line, '.').tail), a);
+		} else if (str_starts(line, S("###"))) {
+			lm = lm_chg(lm, LINE_HDR3, &out, a);
+			str_cat_html(&out, str_trim(str_skip(line, 3)), a);
+		} else if (str_starts(line, S("##"))) {
+			lm = lm_chg(lm, LINE_HDR2, &out, a);
+			str_cat_html(&out, str_trim(str_skip(line, 2)), a);
+		} else if (str_starts(line, S("#"))) {
+			lm = lm_chg(lm, LINE_HDR1, &out, a);
+			title = str_trim(str_skip(line, 1));
+			str_cat_html(&out, title, a);
+		} else if (str_starts(line, S(">"))) {
+			lm = lm_chg(lm, LINE_BQUOT, &out, a);
+			str_cat_html(&out, str_trim(str_skip(line, 1)), a);
+		} else {
+			lm = lm_chg(lm, LINE_PARA, &out, a);
+			str_cat_html(&out, line, a);
+		}
+	}
+	lm = lm_chg(lm, LINE_BLANK, &out, a);
+	Doc *d = new(a, Doc);
+	if (title.s) d->title = str_dup(title, a);
+	d->html = out;
+	d->prev = (*dp);
+	if (*dp) (*dp)->next = d;
+	*dp = d;
+	return 0;
+}
+
+#define ARENA(n, sz) Arena n; { static char arena_backarr[sz];\
+	n.beg = arena_backarr; n.end = arena_backarr + sizeof(arena_backarr);\
+	__asm("":"+r"(n.beg)); __asm("":"+r"(n.end)); }
+
+uint64_t str_hash(Str s) {
+	uint64_t h = 14695981039346656037LU;
+	for (isize i = 0; i < s.n; i++) h = (h ^ (s.s[i] & 0xff)) * 1099511628211LU;
+	return h;
+}
+
+/* --hvar bgcolor:'#fcc,#cfc,#ccf,#cff,#ffc,#fcf' */
+int hvar_calc(Str param, Str *name, Str *val, Str filename) {
+	Cut c = str_cut(param, ':');
+	*name = c.head;
+	usize n = 0;
+	for (Str h = c.tail; h.n > 0; h = str_cut(h, ',').tail) n++;
+	srand(str_hash(filename));
+	usize j = rand() % n;
+	usize i = 0;
+	for (Str h = c.tail; h.n > 0; h = str_cut(h, ',').tail) {
+		if (i == j) {
+			*val = str_cut(h, ',').head;
+			return 0;
+		}
+		i++;
+	}
+	return 1;
+}
+
+#define countof(x) (sizeof(x) / sizeof(*x))
+int main(int argc, const char **argv) {
+	(void)argc;
+
+	ARENA(perm, 1 << 20)
+	ARENA(scratch, 1 << 20)
+
+	Doc *doc = 0;
+	struct {
+		int standalone;
+		int from_stdin;
+		Str stylesheet;
+		Str hvarv[1024];
+		int hvarc;
+	} opts = { 0 };
+	int r;
+
+	ArgsState a = args_begin(argv);
+	Str param = { 0 };
+	opts.from_stdin = 1;
+
+	while ((r = arg_get(&a, "sc:h:", &param, ":css", 'c', ":hvar", 'h')) >= ARG_OK) {
+		Arena reset = scratch;
+		FILE *f;
+		switch (r) {
+		case 's':
+			opts.standalone = 1;
+			break;
+		case 'c':
+			opts.stylesheet = param;
+			break;
+		case 'h':
+			if (opts.hvarc == countof(opts.hvarv)) {
+				fprintf(stderr, "too many hash variables!\n");
+				return 1;
+			}
+			opts.hvarv[opts.hvarc++] = param;
+			break;
+		default:
+			opts.from_stdin = 0;
+			f = fopen(str_to_cstr(param, &scratch), "r/o");
+			if (wdoc(f, &doc, &perm, &scratch)) {
+				fwrite(param.s, 1, param.n, stderr);
+				fprintf(stderr, ": %s\n", strerror(errno));
+				return 1;
+			}
+			fclose(f);
+			break;
+		}
+		scratch = reset;
+	}
+
+	switch (r) {
+	case ARG_BAD:
+		fprintf(stderr, "unknown option '");
+		fwrite(param.s, 1, param.n, stderr);
+		fprintf(stderr, "'\n");
+		return 1;
+	case ARG_EMPTY:
+		fprintf(stderr, "'");
+		fwrite(param.s, 1, param.n, stderr);
+		fprintf(stderr, "' option expected an argument\n");
+		return 1;
+	}
+
+	if (opts.from_stdin) {
+		wdoc(stdin, &doc, &perm, &scratch);
+	}
+
+	if (doc && opts.standalone) {
+		Str title = doc->title;
+		while (doc->prev) {
+			doc = doc->prev;
+			if (doc->title.s) title = doc->title;
+		}
+		Str thtml = S("<!DOCTYPE html>"
+				"<meta charset=utf-8>"
+				"<meta name=viewport content='width=device-width,initial-scale=1'>");
+		if (title.s) {
+			str_cat(&thtml, S("<title>"), &scratch);
+			str_cat_html(&thtml, title, &scratch);
+			str_cat(&thtml, S("</title>"), &scratch);
+		}
+
+		if (opts.stylesheet.s) {
+			FILE *f = fopen(str_to_cstr(opts.stylesheet, &scratch), "r/o");
+			if (!f) {
+				str_putf(opts.stylesheet, stderr);
+				fprintf(stderr, ": %s\n", strerror(errno));
+				return 1;
+			}
+			Str css;
+			Arena p = perm;
+			if (read_all(f, &css, &perm)) {
+				fprintf(stderr, "failed to read stylesheet: %s\n", strerror(errno));
+				return 1;
+			}
+			str_cat(&thtml, S("<style>"), &scratch);
+			if (opts.hvarc > 0) {
+				str_cat(&thtml, S(":root{"), &scratch);
+				for (int i = 0; i < opts.hvarc; i++) {
+					Str name, val;
+					if (hvar_calc(opts.hvarv[i], &name, &val, title)) {
+						fprintf(stderr, "failed to caluclate hashvar!\n");
+						return 1;
+					}
+					str_cat(&thtml, S("--"), &scratch);
+					str_cat(&thtml, name, &scratch);
+					str_catc(&thtml, ':', &scratch);
+					str_cat(&thtml, val, &scratch);
+					str_catc(&thtml, ';', &scratch);
+				}
+				str_catc(&thtml, '}', &scratch);
+			}
+			str_cat(&thtml, css, &scratch);
+			str_cat(&thtml, S("</style>"), &scratch);
+			perm = p;
+		}
+
+		str_put(thtml);
+	}
+
+	while (doc) {
+		str_put(doc->html);
+		doc = doc->next;
+	}
+
+	return 0;
+}
diff --git a/str.h b/str.h
new file mode 100644
index 0000000..22a0d31
--- /dev/null
+++ b/str.h
@@ -0,0 +1,102 @@
+#ifndef STR_H
+#define STR_H
+
+#include <string.h>
+#include <stddef.h>
+
+#include "typ.h"
+#include "arena.h"
+
+typedef struct {
+	char *s;
+	isize n;
+} Str;
+
+#define S(s) (Str){s,sizeof(s)-1}
+
+/* allocating */
+
+Str str_dup(Str a, Arena *m) {
+	char *s = new_arr(m, char, a.n);
+	memcpy(s, a.s, a.n);
+	a.s = s;
+	return a;
+}
+
+static inline void str_cat(Str *a, Str b, Arena *m) {
+	a->s = resize(m, a->s, a->n, a->n + b.n);
+	memcpy(&a->s[a->n], b.s, b.n);
+	a->n += b.n;
+}
+
+/* conversions */
+
+static inline char *str_to_cstr(Str s, Arena *a) {
+	char *r = new_arr(a, char, s.n + 1);
+	memcpy(r, s.s, s.n);
+	r[s.n] = 0;
+	return r;
+}
+
+static inline Str str_from_cstr(const char *s) {
+	return (Str) { (char*)s, strlen(s) };
+}
+
+/* pure functions */
+
+static inline int str_eql(Str a, Str b) {
+	return a.n == b.n && !memcmp(a.s, b.s, b.n);
+}
+
+static inline int str_starts(Str a, Str b) {
+	return a.n >= b.n && !memcmp(a.s, b.s, b.n);
+}
+
+static inline int str_ends(Str a, Str b) {
+	return a.n >= b.n && !memcmp(&a.s[a.n - b.n], b.s, b.n);
+}
+
+static inline void str_catc(Str *a, char b, Arena *m) {
+	a->s = resize(m, a->s, a->n, a->n + 1);
+	a->s[a->n++] = b;
+}
+
+static inline Str str_skip(Str a, isize n) {
+	return (Str) { a.s + n, a.n - n };
+}
+
+static inline int is_space(char c) {
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+static inline Str str_trim_left(Str a) {
+	while (a.n > 0 && is_space(a.s[0])) a.s++, a.n--;
+	return a;
+}
+
+static inline Str str_trim_right(Str a) {
+	while (a.n > 0 && is_space(a.s[a.n - 1])) a.n--;
+	return a;
+}
+
+static inline Str str_trim(Str a) {
+	return str_trim_left(str_trim_right(a));
+}
+
+typedef struct {
+	Str head, tail;
+} Cut;
+
+static inline Cut str_cut(Str s, char c) {
+	char *p = memchr(s.s, c, s.n);
+	if (!p) {
+		return (Cut) { s, { &s.s[s.n], 0 } };
+	} else {
+		return (Cut) {
+			{ s.s, p - s.s },
+			{ p + 1, &s.s[s.n] - (p + 1) }
+		};
+	}
+}
+
+#endif
diff --git a/typ.h b/typ.h
new file mode 100644
index 0000000..9a5d2b5
--- /dev/null
+++ b/typ.h
@@ -0,0 +1,7 @@
+#ifndef TYP_H
+#define TYP_H
+
+typedef size_t usize;
+typedef ptrdiff_t isize;
+
+#endif