diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cbits/array.c b/cbits/array.c
new file mode 100644
--- /dev/null
+++ b/cbits/array.c
@@ -0,0 +1,300 @@
+/* array.c - automatic dynamic array for pointers */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "array.h"
+
+#include <string.h>
+
+
+/***************************
+ * STATIC HELPER FUNCTIONS *
+ ***************************/
+
+/* arr_realloc • realloc memory of a struct array */
+static int
+arr_realloc(struct array* arr, int neosz) {
+	void* neo;
+	neo = realloc(arr->base, neosz * arr->unit);
+	if (neo == 0) return 0;
+	arr->base = neo;
+	arr->asize = neosz;
+	if (arr->size > neosz) arr->size = neosz;
+	return 1; }
+
+
+/* parr_realloc • realloc memory of a struct parray */
+static int
+parr_realloc(struct parray* arr, int neosz) {
+	void* neo;
+	neo = realloc(arr->item, neosz * sizeof (void*));
+	if (neo == 0) return 0;
+	arr->item = neo;
+	arr->asize = neosz;
+	if (arr->size > neosz) arr->size = neosz;
+	return 1; }
+
+
+
+/***************************
+ * GENERIC ARRAY FUNCTIONS *
+ ***************************/
+
+/* arr_adjust • shrink the allocated memory to fit exactly the needs */
+int
+arr_adjust(struct array *arr) {
+	return arr_realloc(arr, arr->size); }
+
+
+/* arr_free • frees the structure contents (buf NOT the struct itself) */
+void
+arr_free(struct array *arr) {
+	if (!arr) return;
+	free(arr->base);
+	arr->base = 0;
+	arr->size = arr->asize = 0; }
+
+
+/* arr_grow • increases the array size to fit the given number of elements */
+int
+arr_grow(struct array *arr, int need) {
+	if (arr->asize >= need) return 1;
+	else return arr_realloc(arr, need); }
+
+
+/* arr_init • initialization of the contents of the struct */
+void
+arr_init(struct array *arr, size_t unit) {
+	arr->base = 0;
+	arr->size = arr->asize = 0;
+	arr->unit = unit; }
+
+
+/* arr_insert • inserting nb elements before the nth one */
+int
+arr_insert(struct array *arr, int nb, int n) {
+	char *src, *dst;
+	size_t len;
+	if (!arr || nb <= 0 || n < 0
+	|| !arr_grow(arr, arr->size + nb))
+		return 0;
+	if (n < arr->size) {
+		src = arr->base;
+		src += n * arr->unit;
+		dst = src + nb * arr->unit;
+		len = (arr->size - n) * arr->unit;
+		memmove(dst, src, len); }
+	arr->size += nb;
+	return 1; }
+
+
+/* arr_item • returns a pointer to the n-th element */
+void *
+arr_item(struct array *arr, int no) {
+	char *ptr;
+	if (!arr || no < 0 || no >= arr->size) return 0;
+	ptr = arr->base;
+	ptr += no * arr->unit;
+	return ptr; }
+
+
+/* arr_newitem • returns the index of a new element appended to the array */
+int
+arr_newitem(struct array *arr) {
+	if (!arr_grow(arr, arr->size + 1)) return -1;
+	arr->size += 1;
+	return arr->size - 1; }
+
+
+/* arr_remove • removes the n-th elements of the array */
+void
+arr_remove(struct array *arr, int idx) {
+	if (!arr || idx < 0 || idx >= arr->size) return;
+	arr->size -= 1;
+	if (idx < arr->size) {
+		char *dst = arr->base;
+		char *src;
+		dst += idx * arr->unit;
+		src = dst + arr->unit;
+		memmove(dst, src, (arr->size - idx) * arr->unit); } }
+
+
+/* arr_sorted_find • O(log n) search in a sorted array, returning entry */
+void *
+arr_sorted_find(struct array *arr, void *key, array_cmp_fn cmp) {
+	int mi, ma, cu, ret;
+	char *ptr = arr->base;
+	mi = -1;
+	ma = arr->size;
+	while (mi < ma - 1) {
+		cu = mi + (ma - mi) / 2;
+		ret = cmp(key, ptr + cu * arr->unit);
+		if (ret == 0) return ptr + cu * arr->unit;
+		else if (ret < 0) ma = cu;
+		else /* if (ret > 0) */ mi = cu; }
+	return 0; }
+
+
+/* arr_sorted_find_i • O(log n) search in a sorted array,
+ *      returning index of the smallest element larger than the key */
+int
+arr_sorted_find_i(struct array *arr, void *key, array_cmp_fn cmp) {
+	int mi, ma, cu, ret;
+	char *ptr = arr->base;
+	mi = -1;
+	ma = arr->size;
+	while (mi < ma - 1) {
+		cu = mi + (ma - mi) / 2;
+		ret = cmp(key, ptr + cu * arr->unit);
+		if (ret == 0) {
+			while (cu < arr->size && ret == 0) {
+				cu += 1;
+				ret = cmp(key, ptr + cu * arr->unit); }
+			return cu; }
+		else if (ret < 0) ma = cu;
+		else /* if (ret > 0) */ mi = cu; }
+	return ma; }
+
+
+
+/***************************
+ * POINTER ARRAY FUNCTIONS *
+ ***************************/
+
+/* parr_adjust • shrinks the allocated memory to fit exactly the needs */
+int
+parr_adjust(struct parray* arr) {
+	return parr_realloc (arr, arr->size); }
+
+
+/* parr_free • frees the structure contents (buf NOT the struct itself) */
+void
+parr_free(struct parray *arr) {
+	if (!arr) return;
+	free (arr->item);
+	arr->item = 0;
+	arr->size = 0;
+	arr->asize = 0; }
+
+
+/* parr_grow • increases the array size to fit the given number of elements */
+int
+parr_grow(struct parray *arr, int need) {
+	if (arr->asize >= need) return 1;
+	else return parr_realloc (arr, need); }
+
+
+/* parr_init • initialization of the struct (which is equivalent to zero) */
+void
+parr_init(struct parray *arr) {
+	arr->item = 0;
+	arr->size = 0;
+	arr->asize = 0; }
+
+
+/* parr_insert • inserting nb elements before the nth one */
+int
+parr_insert(struct parray *parr, int nb, int n) {
+	char *src, *dst;
+	size_t len, i;
+	if (!parr || nb <= 0 || n < 0
+	|| !parr_grow(parr, parr->size + nb))
+		return 0;
+	if (n < parr->size) {
+		src = (void *)parr->item;
+		src += n * sizeof (void *);
+		dst = src + nb * sizeof (void *);
+		len = (parr->size - n) * sizeof (void *);
+		memmove(dst, src, len);
+		for (i = 0; i < (size_t)nb; ++i)
+			parr->item[n + i] = 0; }
+	parr->size += nb;
+	return 1; }
+
+
+/* parr_pop • pops the last item of the array and returns it */
+void *
+parr_pop(struct parray *arr) {
+	if (arr->size <= 0) return 0;
+	arr->size -= 1;
+	return arr->item[arr->size]; }
+
+
+/* parr_push • pushes a pointer at the end of the array (= append) */
+int
+parr_push(struct parray *arr, void *i) {
+	if (!parr_grow(arr, arr->size + 1)) return 0;
+	arr->item[arr->size] = i;
+	arr->size += 1;
+	return 1; }
+
+
+/* parr_remove • removes the n-th element of the array and returns it */
+void *
+parr_remove(struct parray *arr, int idx) {
+	void* ret;
+	int i;
+	if (!arr || idx < 0 || idx >= arr->size) return 0;
+	ret = arr->item[idx];
+	for (i = idx+1; i < arr->size; ++i)
+		arr->item[i - 1] = arr->item[i];
+	arr->size -= 1;
+	return ret; }
+
+
+/* parr_sorted_find • O(log n) search in a sorted array, returning entry */
+void *
+parr_sorted_find(struct parray *arr, void *key, array_cmp_fn cmp) {
+	int mi, ma, cu, ret;
+	mi = -1;
+	ma = arr->size;
+	while (mi < ma - 1) {
+		cu = mi + (ma - mi) / 2;
+		ret = cmp(key, arr->item[cu]);
+		if (ret == 0) return arr->item[cu];
+		else if (ret < 0) ma = cu;
+		else /* if (ret > 0) */ mi = cu; }
+	return 0; }
+
+
+/* parr_sorted_find_i • O(log n) search in a sorted array,
+ *      returning index of the smallest element larger than the key */
+int
+parr_sorted_find_i(struct parray *arr, void *key, array_cmp_fn cmp) {
+	int mi, ma, cu, ret;
+	mi = -1;
+	ma = arr->size;
+	while (mi < ma - 1) {
+		cu = mi + (ma - mi) / 2;
+		ret = cmp(key, arr->item[cu]);
+		if (ret == 0) {
+			while (cu < arr->size && ret == 0) {
+				cu += 1;
+				ret = cmp(key, arr->item[cu]); }
+			return cu; }
+		else if (ret < 0) ma = cu;
+		else /* if (ret > 0) */ mi = cu; }
+	return ma; }
+
+
+/* parr_top • returns the top the stack (i.e. the last element of the array) */
+void *
+parr_top(struct parray *arr) {
+	if (arr == 0 || arr->size <= 0) return 0;
+	else return arr->item[arr->size - 1]; }
+
+/* vim: set filetype=c: */
diff --git a/cbits/array.h b/cbits/array.h
new file mode 100644
--- /dev/null
+++ b/cbits/array.h
@@ -0,0 +1,147 @@
+/* array.h - automatic dynamic array for pointers */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LITHIUM_ARRAY_H
+#define LITHIUM_ARRAY_H
+
+#include <stdlib.h>
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* struct array • generic linear array */
+struct array {
+	void*	base;
+	int	size;
+	int	asize;
+	size_t	unit; };
+
+
+/* struct parray • array of pointers */
+struct parray {
+	void **	item;
+	int	size;
+	int	asize; };
+
+
+/* array_cmp_fn • comparison functions for sorted arrays */
+typedef int (*array_cmp_fn)(void *key, void *array_entry);
+
+
+
+/***************************
+ * GENERIC ARRAY FUNCTIONS *
+ ***************************/
+
+/* arr_adjust • shrink the allocated memory to fit exactly the needs */
+int
+arr_adjust(struct array *);
+
+/* arr_free • frees the structure contents (buf NOT the struct itself) */
+void
+arr_free(struct array *);
+
+/* arr_grow • increases the array size to fit the given number of elements */
+int
+arr_grow(struct array *, int);
+
+/* arr_init • initialization of the contents of the struct */
+void
+arr_init(struct array *, size_t);
+
+/* arr_insert • inserting elements nb before the nth one */
+int
+arr_insert(struct array *, int nb, int n);
+
+/* arr_item • returns a pointer to the n-th element */
+void *
+arr_item(struct array *, int);
+
+/* arr_newitem • returns the index of a new element appended to the array */
+int
+arr_newitem(struct array *);
+
+/* arr_remove • removes the n-th elements of the array */
+void
+arr_remove(struct array *, int);
+
+/* arr_sorted_find • O(log n) search in a sorted array, returning entry */
+/* equivalent to bsearch(key, arr->base, arr->size, arr->unit, cmp) */
+void *
+arr_sorted_find(struct array *, void *key, array_cmp_fn cmp);
+
+/* arr_sorted_find_i • O(log n) search in a sorted array,
+ *      returning index of the smallest element larger than the key */
+int
+arr_sorted_find_i(struct array *, void *key, array_cmp_fn cmp);
+
+
+/***************************
+ * POINTER ARRAY FUNCTIONS *
+ ***************************/
+
+/* parr_adjust • shrinks the allocated memory to fit exactly the needs */
+int
+parr_adjust(struct parray *);
+
+/* parr_free • frees the structure contents (buf NOT the struct itself) */
+void
+parr_free(struct parray *);
+
+/* parr_grow • increases the array size to fit the given number of elements */
+int
+parr_grow(struct parray *, int);
+
+/* parr_init • initialization of the struct (which is equivalent to zero) */
+void
+parr_init(struct parray *);
+
+/* parr_insert • inserting nb elements before the nth one */
+int
+parr_insert(struct parray *, int nb, int n);
+
+/* parr_pop • pops the last item of the array and returns it */
+void *
+parr_pop(struct parray *);
+
+/* parr_push • pushes a pointer at the end of the array (= append) */
+int
+parr_push(struct parray *, void *);
+
+/* parr_remove • removes the n-th element of the array and returns it */
+void *
+parr_remove(struct parray *, int);
+
+/* parr_sorted_find • O(log n) search in a sorted array, returning entry */
+void *
+parr_sorted_find(struct parray *, void *key, array_cmp_fn cmp);
+
+/* parr_sorted_find_i • O(log n) search in a sorted array,
+ *      returning index of the smallest element larger than the key */
+int
+parr_sorted_find_i(struct parray *, void *key, array_cmp_fn cmp);
+
+/* parr_top • returns the top the stack (i.e. the last element of the array) */
+void *
+parr_top(struct parray *);
+
+
+#endif /* ndef LITHIUM_ARRAY_H */
+
+/* vim: set filetype=c: */
diff --git a/cbits/autolink.c b/cbits/autolink.c
new file mode 100644
--- /dev/null
+++ b/cbits/autolink.c
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "buffer.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+int
+sd_autolink_issafe(const char *link, size_t link_len)
+{
+	static const size_t valid_uris_count = 4;
+	static const char *valid_uris[] = {
+		"http://", "https://", "ftp://", "mailto://"
+	};
+
+	size_t i;
+
+	for (i = 0; i < valid_uris_count; ++i) {
+		size_t len = strlen(valid_uris[i]);
+
+		if (link_len > len &&
+			strncasecmp(link, valid_uris[i], len) == 0 &&
+			isalnum(link[len]))
+			return 1;
+	}
+
+	return 0;
+}
+
+static size_t
+autolink_delim(char *data, size_t link_end, size_t offset, size_t size)
+{
+	char cclose, copen = 0;
+	size_t i;
+
+	for (i = 0; i < link_end; ++i)
+		if (data[i] == '<') {
+			link_end = i;
+			break;
+		}
+
+	while (link_end > 0) {
+		if (strchr("?!.,", data[link_end - 1]) != NULL)
+			link_end--;
+
+		else if (data[link_end - 1] == ';') {
+			size_t new_end = link_end - 2;
+
+			while (new_end > 0 && isalpha(data[new_end]))
+				new_end--;
+
+			if (new_end < link_end - 2 && data[new_end] == '&')
+				link_end = new_end;
+			else
+				link_end--;
+		}
+		else break;
+	}
+
+	if (link_end == 0)
+		return 0;
+
+	cclose = data[link_end - 1];
+
+	switch (cclose) {
+	case '"':	copen = '"'; break;
+	case '\'':	copen = '\''; break;
+	case ')':	copen = '('; break;
+	case ']':	copen = '['; break;
+	case '}':	copen = '{'; break;
+	}
+
+	if (copen != 0) {
+		size_t closing = 0;
+		size_t opening = 0;
+		size_t i = 0;
+
+		/* Try to close the final punctuation sign in this same line;
+		 * if we managed to close it outside of the URL, that means that it's
+		 * not part of the URL. If it closes inside the URL, that means it
+		 * is part of the URL.
+		 *
+		 * Examples:
+		 *
+		 *	foo http://www.pokemon.com/Pikachu_(Electric) bar
+		 *		=> http://www.pokemon.com/Pikachu_(Electric)
+		 *
+		 *	foo (http://www.pokemon.com/Pikachu_(Electric)) bar
+		 *		=> http://www.pokemon.com/Pikachu_(Electric)
+		 *
+		 *	foo http://www.pokemon.com/Pikachu_(Electric)) bar
+		 *		=> http://www.pokemon.com/Pikachu_(Electric))
+		 *
+		 *	(foo http://www.pokemon.com/Pikachu_(Electric)) bar
+		 *		=> foo http://www.pokemon.com/Pikachu_(Electric)
+		 */
+
+		while (i < link_end) {
+			if (data[i] == copen)
+				opening++;
+			else if (data[i] == cclose)
+				closing++;
+
+			i++;
+		}
+
+		if (closing != opening)
+			link_end--;
+	}
+
+	return link_end;
+}
+
+static size_t
+check_domain(char *data, size_t size)
+{
+	size_t i, np = 0;
+
+	if (!isalnum(data[0]))
+		return 0;
+
+	for (i = 1; i < size - 1; ++i) {
+		if (data[i] == '.') np++;
+		else if (!isalnum(data[i]) && data[i] != '-') break;
+	}
+
+	if (!isalnum(data[i - 1]) || np == 0)
+		return 0;
+
+	return i;
+}
+
+size_t
+sd_autolink__www(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+	size_t link_end;
+
+	if (offset > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
+		return 0;
+
+	if (size < 4 || memcmp(data, "www.", STRLEN("www.")) != 0)
+		return 0;
+
+	link_end = check_domain(data, size);
+
+	if (link_end == 0)
+		return 0;
+
+	while (link_end < size && !isspace(data[link_end]))
+		link_end++;
+
+	link_end = autolink_delim(data, link_end, offset, size);
+
+	if (link_end == 0)
+		return 0;
+
+	bufput(link, data, link_end);
+	*rewind_p = 0;
+
+	return (int)link_end;
+}
+
+size_t
+sd_autolink__email(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+	size_t link_end, rewind;
+	int nb = 0, np = 0;
+
+	for (rewind = 0; rewind < offset; ++rewind) {
+		char c = data[-rewind - 1];
+
+		if (isalnum(c))
+			continue;
+
+		if (strchr(".+-_", c) != NULL)
+			continue;
+
+		break;
+	}
+
+	if (rewind == 0)
+		return 0;
+
+	for (link_end = 0; link_end < size; ++link_end) {
+		char c = data[link_end];
+
+		if (isalnum(c))
+			continue;
+
+		if (c == '@')
+			nb++;
+		else if (c == '.' && link_end < size - 1)
+			np++;
+		else if (c != '-' && c != '_')
+			break;
+	}
+
+	if (link_end < 2 || nb != 1 || np == 0)
+		return 0;
+
+	link_end = autolink_delim(data, link_end, offset, size);
+
+	if (link_end == 0)
+		return 0;
+
+	bufput(link, data - rewind, link_end + rewind);
+	*rewind_p = rewind;
+
+	return link_end;
+}
+
+size_t
+sd_autolink__url(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size)
+{
+	size_t link_end, rewind = 0, domain_len;
+
+	if (size < 4 || data[1] != '/' || data[2] != '/')
+		return 0;
+
+	while (rewind < offset && isalpha(data[-rewind - 1]))
+		rewind++;
+
+	if (!sd_autolink_issafe(data - rewind, size + rewind))
+		return 0;
+	link_end = STRLEN("://");
+
+	domain_len = check_domain(data + link_end, size - link_end);
+	if (domain_len == 0)
+		return 0;
+
+	link_end += domain_len;
+	while (link_end < size && !isspace(data[link_end]))
+		link_end++;
+
+	link_end = autolink_delim(data, link_end, offset, size);
+
+	if (link_end == 0)
+		return 0;
+
+	bufput(link, data - rewind, link_end + rewind);
+	*rewind_p = rewind;
+
+	return link_end;
+}
+
diff --git a/cbits/autolink.h b/cbits/autolink.h
new file mode 100644
--- /dev/null
+++ b/cbits/autolink.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UPSKIRT_AUTOLINK_H
+#define UPSKIRT_AUTOLINK_H
+
+#include "buffer.h"
+
+extern int
+sd_autolink_issafe(const char *link, size_t link_len);
+
+extern size_t
+sd_autolink__www(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+extern size_t
+sd_autolink__email(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+extern size_t
+sd_autolink__url(size_t *rewind_p, struct buf *link, char *data, size_t offset, size_t size);
+
+#endif
+
+/* vim: set filetype=c: */
diff --git a/cbits/buffer.c b/cbits/buffer.c
new file mode 100644
--- /dev/null
+++ b/cbits/buffer.c
@@ -0,0 +1,323 @@
+/* buffer.c - automatic buffer structure */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * COMPILE TIME OPTIONS
+ *
+ * BUFFER_STATS • if defined, stats are kept about memory usage
+ */
+
+#define BUFFER_STDARG
+#define BUFFER_MAX_ALLOC_SIZE (1024 * 1024 * 16) //16mb
+
+#include "buffer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+/********************
+ * GLOBAL VARIABLES *
+ ********************/
+
+#ifdef BUFFER_STATS
+long buffer_stat_nb = 0;
+size_t buffer_stat_alloc_bytes = 0;
+#endif
+
+
+/***************************
+ * STATIC HELPER FUNCTIONS *
+ ***************************/
+
+/* lower • retruns the lower-case variant of the input char */
+static char
+lower(char c) {
+	return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c; }
+
+
+
+/********************
+ * BUFFER FUNCTIONS *
+ ********************/
+
+/* bufcasecmp • case-insensitive buffer comparison */
+int
+bufcasecmp(const struct buf *a, const struct buf *b) {
+	size_t i = 0;
+	size_t cmplen;
+	if (a == b) return 0;
+	if (!a) return -1; else if (!b) return 1;
+	cmplen = (a->size < b->size) ? a->size : b->size;
+	while (i < cmplen && lower(a->data[i]) == lower(b->data[i])) ++i;
+	if (i < a->size) {
+		if (i < b->size)  return lower(a->data[i]) - lower(b->data[i]);
+		else		  return 1; }
+	else {	if (i < b->size)  return -1;
+		else		  return 0; } }
+
+
+/* bufcmp • case-sensitive buffer comparison */
+int
+bufcmp(const struct buf *a, const struct buf *b) {
+	size_t i = 0;
+	size_t cmplen;
+	if (a == b) return 0;
+	if (!a) return -1; else if (!b) return 1;
+	cmplen = (a->size < b->size) ? a->size : b->size;
+	while (i < cmplen && a->data[i] == b->data[i]) ++i;
+	if (i < a->size) {
+		if (i < b->size)	return a->data[i] - b->data[i];
+		else			return 1; }
+	else {	if (i < b->size)	return -1;
+		else			return 0; } }
+
+
+/* bufcmps • case-sensitive comparison of a string to a buffer */
+int
+bufcmps(const struct buf *a, const char *b) {
+	const size_t len = strlen(b);
+	size_t cmplen = len;
+	int r;
+	if (!a || !a->size) return b ? 0 : -1;
+	if (len < a->size) cmplen = a->size;
+	r = strncmp(a->data, b, cmplen);
+	if (r) return r;
+	else if (a->size == len) return 0;
+	else if (a->size < len) return -1;
+	else return 1; }
+
+int
+bufprefix(const struct buf *buf, const char *prefix)
+{
+	size_t i;
+
+	for (i = 0; i < buf->size; ++i) {
+		if (prefix[i] == 0)
+			return 0;
+
+		if (buf->data[i] != prefix[i])
+			return buf->data[i] - prefix[i];
+	}
+
+	return 0;
+}
+
+
+/* bufdup • buffer duplication */
+struct buf *
+bufdup(const struct buf *src, size_t dupunit) {
+	size_t blocks;
+	struct buf *ret;
+	if (src == 0) return 0;
+	ret = malloc(sizeof (struct buf));
+	if (ret == 0) return 0;
+	ret->unit = dupunit;
+	ret->size = src->size;
+	ret->ref = 1;
+	if (!src->size) {
+		ret->asize = 0;
+		ret->data = 0;
+		return ret; }
+	blocks = (src->size + dupunit - 1) / dupunit;
+	ret->asize = blocks * dupunit;
+	ret->data = malloc(ret->asize);
+	if (ret->data == 0) {
+		free(ret);
+		return 0; }
+	memcpy(ret->data, src->data, src->size);
+#ifdef BUFFER_STATS
+	buffer_stat_nb += 1;
+	buffer_stat_alloc_bytes += ret->asize;
+#endif
+	return ret; }
+
+/* bufgrow • increasing the allocated size to the given value */
+int
+bufgrow(struct buf *buf, size_t neosz) {
+	size_t neoasz;
+	void *neodata;
+	if (!buf || !buf->unit || neosz > BUFFER_MAX_ALLOC_SIZE) return 0;
+	if (buf->asize >= neosz) return 1;
+	neoasz = buf->asize + buf->unit;
+	while (neoasz < neosz) neoasz += buf->unit;
+	neodata = realloc(buf->data, neoasz);
+	if (!neodata) return 0;
+#ifdef BUFFER_STATS
+	buffer_stat_alloc_bytes += (neoasz - buf->asize);
+#endif
+	buf->data = neodata;
+	buf->asize = neoasz;
+	return 1; }
+
+
+/* bufnew • allocation of a new buffer */
+struct buf *
+bufnew(size_t unit) {
+	struct buf *ret;
+	ret = malloc(sizeof (struct buf));
+	if (ret) {
+#ifdef BUFFER_STATS
+		buffer_stat_nb += 1;
+#endif
+		ret->data = 0;
+		ret->size = ret->asize = 0;
+		ret->ref = 1;
+		ret->unit = unit; }
+	return ret; }
+
+
+/* bufnullterm • NUL-termination of the string array (making a C-string) */
+void
+bufnullterm(struct buf *buf) {
+	if (!buf || !buf->unit) return;
+	if (buf->size < buf->asize && buf->data[buf->size] == 0) return;
+	if (buf->size + 1 <= buf->asize || bufgrow(buf, buf->size + 1))
+		buf->data[buf->size] = 0; }
+
+
+/* bufprintf • formatted printing to a buffer */
+void
+bufprintf(struct buf *buf, const char *fmt, ...) {
+	va_list ap;
+	if (!buf || !buf->unit) return;
+	va_start(ap, fmt);
+	vbufprintf(buf, fmt, ap);
+	va_end(ap); }
+
+
+/* bufput • appends raw data to a buffer */
+void
+bufput(struct buf *buf, const void *data, size_t len) {
+	if (!buf) return;
+	if (buf->size + len > buf->asize && !bufgrow(buf, buf->size + len))
+		return;
+	memcpy(buf->data + buf->size, data, len);
+	buf->size += len; }
+
+
+/* bufputs • appends a NUL-terminated string to a buffer */
+void
+bufputs(struct buf *buf, const char *str) {
+	bufput(buf, str, strlen (str)); }
+
+
+/* bufputc • appends a single char to a buffer */
+void
+bufputc(struct buf *buf, char c) {
+	if (!buf) return;
+	if (buf->size + 1 > buf->asize && !bufgrow(buf, buf->size + 1))
+		return;
+	buf->data[buf->size] = c;
+	buf->size += 1; }
+
+
+/* bufrelease • decrease the reference count and free the buffer if needed */
+void
+bufrelease(struct buf *buf) {
+	if (!buf) return;
+	buf->ref -= 1;
+	if (buf->ref == 0) {
+#ifdef BUFFER_STATS
+		buffer_stat_nb -= 1;
+		buffer_stat_alloc_bytes -= buf->asize;
+#endif
+		free(buf->data);
+		free(buf); } }
+
+
+/* bufreset • frees internal data of the buffer */
+void
+bufreset(struct buf *buf) {
+	if (!buf) return;
+#ifdef BUFFER_STATS
+	buffer_stat_alloc_bytes -= buf->asize;
+#endif
+	free(buf->data);
+	buf->data = 0;
+	buf->size = buf->asize = 0; }
+
+
+/* bufset • safely assigns a buffer to another */
+void
+bufset(struct buf **dest, struct buf *src) {
+	if (src) {
+		if (!src->asize) src = bufdup(src, 1);
+		else src->ref += 1; }
+	bufrelease(*dest);
+	*dest = src; }
+
+
+/* bufslurp • removes a given number of bytes from the head of the array */
+void
+bufslurp(struct buf *buf, size_t len) {
+	if (!buf || !buf->unit || len <= 0) return;
+	if (len >= buf->size) {
+		buf->size = 0;
+		return; }
+	buf->size -= len;
+	memmove(buf->data, buf->data + len, buf->size); }
+
+
+/* buftoi • converts the numbers at the beginning of the buf into an int */
+int
+buftoi(struct buf *buf, size_t offset_i, size_t *offset_o) {
+	int r = 0, neg = 0;
+	size_t i = offset_i;
+	if (!buf || !buf->size) return 0;
+	if (buf->data[i] == '+') i += 1;
+	else if (buf->data[i] == '-') {
+		neg = 1;
+		i += 1; }
+	while (i < buf->size && buf->data[i] >= '0' && buf->data[i] <= '9') {
+		r = (r * 10) + buf->data[i] - '0';
+		i += 1; }
+	if (offset_o) *offset_o = i;
+	return neg ? -r : r; }
+
+
+
+/* vbufprintf • stdarg variant of formatted printing into a buffer */
+void
+vbufprintf(struct buf *buf, const char *fmt, va_list ap) {
+	int n;
+	va_list ap_save;
+	if (buf == 0
+	|| (buf->size >= buf->asize && !bufgrow (buf, buf->size + 1)))
+		return;
+
+	va_copy(ap_save, ap);
+	n = vsnprintf(buf->data + buf->size, buf->asize - buf->size, fmt, ap);
+
+	if (n < 0 || (size_t)n >= buf->asize - buf->size) {
+		size_t new_size = (n > 0) ? n : buf->size;
+		if (!bufgrow (buf, buf->size + new_size + 1))
+			return;
+
+		n = vsnprintf(buf->data + buf->size, buf->asize - buf->size, fmt, ap_save);
+	}
+	va_end(ap_save);
+
+	if (n < 0)
+		return;
+
+	buf->size += n;
+}
+
+/* vim: set filetype=c: */
diff --git a/cbits/buffer.h b/cbits/buffer.h
new file mode 100644
--- /dev/null
+++ b/cbits/buffer.h
@@ -0,0 +1,154 @@
+/* buffer.h - automatic buffer structure */
+
+/*
+ * Copyright (c) 2008, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LITHIUM_BUFFER_H
+#define LITHIUM_BUFFER_H
+
+#include <stddef.h>
+
+#if defined(_MSC_VER)
+#define __attribute__(x)
+#define inline
+#define strncasecmp _strnicmp
+#define snprintf _snprintf
+#define va_copy(d,s) ((d) = (s))
+#endif
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* struct buf • character array buffer */
+struct buf {
+	char *	data;	/* actual character data */
+	size_t	size;	/* size of the string */
+	size_t	asize;	/* allocated size (0 = volatile buffer) */
+	size_t	unit;	/* reallocation unit size (0 = read-only buffer) */
+	int	ref; };	/* reference count */
+
+/**********
+ * MACROS *
+ **********/
+
+#define STRLEN(x) (sizeof(x) - 1)
+
+/* CONST_BUF • global buffer from a string litteral */
+#define CONST_BUF(name, string) \
+	static struct buf name = { string, sizeof string -1, sizeof string }
+
+
+/* VOLATILE_BUF • macro for creating a volatile buffer on the stack */
+#define VOLATILE_BUF(name, strname) \
+	struct buf name = { strname, strlen(strname) }
+
+
+/* BUFPUTSL • optimized bufputs of a string litteral */
+#define BUFPUTSL(output, litteral) \
+	bufput(output, litteral, sizeof litteral - 1)
+
+
+
+/********************
+ * BUFFER FUNCTIONS *
+ ********************/
+
+/* bufcasecmp • case-insensitive buffer comparison */
+int
+bufcasecmp(const struct buf *, const struct buf *);
+
+/* bufcmp • case-sensitive buffer comparison */
+int
+bufcmp(const struct buf *, const struct buf *);
+
+/* bufcmps • case-sensitive comparison of a string to a buffer */
+int
+bufcmps(const struct buf *, const char *);
+
+/* bufprefix * compare the beginning of a buffer with a string */
+int
+bufprefix(const struct buf *buf, const char *prefix);
+
+/* bufdup • buffer duplication */
+struct buf *
+bufdup(const struct buf *, size_t)
+	__attribute__ ((malloc));
+
+/* bufgrow • increasing the allocated size to the given value */
+int
+bufgrow(struct buf *, size_t);
+
+/* bufnew • allocation of a new buffer */
+struct buf *
+bufnew(size_t)
+	__attribute__ ((malloc));
+
+/* bufnullterm • NUL-termination of the string array (making a C-string) */
+void
+bufnullterm(struct buf *);
+
+/* bufprintf • formatted printing to a buffer */
+void
+bufprintf(struct buf *, const char *, ...)
+	__attribute__ ((format (printf, 2, 3)));
+
+/* bufput • appends raw data to a buffer */
+void
+bufput(struct buf *, const void*, size_t);
+
+/* bufputs • appends a NUL-terminated string to a buffer */
+void
+bufputs(struct buf *, const char*);
+
+/* bufputc • appends a single char to a buffer */
+void
+bufputc(struct buf *, char);
+
+/* bufrelease • decrease the reference count and free the buffer if needed */
+void
+bufrelease(struct buf *);
+
+/* bufreset • frees internal data of the buffer */
+void
+bufreset(struct buf *);
+
+/* bufset • safely assigns a buffer to another */
+void
+bufset(struct buf **, struct buf *);
+
+/* bufslurp • removes a given number of bytes from the head of the array */
+void
+bufslurp(struct buf *, size_t);
+
+/* buftoi • converts the numbers at the beginning of the buf into an int */
+int
+buftoi(struct buf *, size_t, size_t *);
+
+
+
+#ifdef BUFFER_STDARG
+#include <stdarg.h>
+
+/* vbufprintf • stdarg variant of formatted printing into a buffer */
+void
+vbufprintf(struct buf *, const char*, va_list);
+
+#endif /* def BUFFER_STDARG */
+
+#endif /* ndef LITHIUM_BUFFER_H */
+
+/* vim: set filetype=c: */
diff --git a/cbits/html.c b/cbits/html.c
new file mode 100644
--- /dev/null
+++ b/cbits/html.c
@@ -0,0 +1,674 @@
+/*
+ * Copyright (c) 2009, Natacha Porté
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "markdown.h"
+#include "html.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#define USE_XHTML(opt) (opt->flags & HTML_USE_XHTML)
+
+static inline void
+put_scaped_char(struct buf *ob, char c)
+{
+	switch (c) {
+		case '<': BUFPUTSL(ob, "&lt;"); break;
+		case '>': BUFPUTSL(ob, "&gt;"); break;
+		case '&': BUFPUTSL(ob, "&amp;"); break;
+		case '"': BUFPUTSL(ob, "&quot;"); break;
+		default: bufputc(ob, c); break;
+	}
+}
+
+/* sdhtml_escape • copy the buffer entity-escaping '<', '>', '&' and '"' */
+void
+sdhtml_escape(struct buf *ob, const char *src, size_t size)
+{
+	size_t  i = 0, org;
+	while (i < size) {
+		/* copying directly unescaped characters */
+		org = i;
+		while (i < size && src[i] != '<' && src[i] != '>'
+		&& src[i] != '&' && src[i] != '"')
+			i += 1;
+		if (i > org) bufput(ob, src + org, i - org);
+
+		/* escaping */
+		if (i >= size) break;
+
+		put_scaped_char(ob, src[i]);
+		i++;
+	}
+}
+
+int
+sdhtml_tag(const char *tag_data, size_t tag_size, const char *tagname)
+{
+	size_t i;
+	int closed = 0;
+
+	if (tag_size < 3 || tag_data[0] != '<')
+		return HTML_TAG_NONE;
+
+	i = 1;
+
+	if (tag_data[i] == '/') {
+		closed = 1;
+		i++;
+	}
+
+	for (; i < tag_size; ++i, ++tagname) {
+		if (*tagname == 0)
+			break;
+
+		if (tag_data[i] != *tagname)
+			return HTML_TAG_NONE;
+	}
+
+	if (i == tag_size)
+		return HTML_TAG_NONE;
+
+	if (isspace(tag_data[i]) || tag_data[i] == '>')
+		return closed ? HTML_TAG_CLOSE : HTML_TAG_OPEN;
+
+	return HTML_TAG_NONE;
+}
+
+/********************
+ * GENERIC RENDERER *
+ ********************/
+static int
+rndr_autolink(struct buf *ob, struct buf *link, enum mkd_autolink type, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	if (!link || !link->size)
+		return 0;
+
+	if ((options->flags & HTML_SAFELINK) != 0 &&
+		!sd_autolink_issafe(link->data, link->size) &&
+		type != MKDA_EMAIL)
+		return 0;
+
+	BUFPUTSL(ob, "<a href=\"");
+	if (type == MKDA_EMAIL)
+		BUFPUTSL(ob, "mailto:");
+	sdhtml_escape(ob, link->data, link->size);
+
+	if (options->link_attributes) {
+		bufputc(ob, '\"');
+		options->link_attributes(ob, link, opaque);
+		bufputc(ob, '>');
+	} else {
+		BUFPUTSL(ob, "\">");
+	}
+
+	/*
+	 * Pretty printing: if we get an email address as
+	 * an actual URI, e.g. `mailto:foo@bar.com`, we don't
+	 * want to print the `mailto:` prefix
+	 */
+	if (bufprefix(link, "mailto:") == 0) {
+		sdhtml_escape(ob, link->data + 7, link->size - 7);
+	} else {
+		sdhtml_escape(ob, link->data, link->size);
+	}
+
+	BUFPUTSL(ob, "</a>");
+
+	return 1;
+}
+
+static void
+rndr_blockcode(struct buf *ob, struct buf *text, struct buf *lang, void *opaque)
+{
+	if (ob->size) bufputc(ob, '\n');
+
+	if (lang && lang->size) {
+		size_t i, cls;
+		BUFPUTSL(ob, "<pre><code class=\"");
+
+		for (i = 0, cls = 0; i < lang->size; ++i, ++cls) {
+			while (i < lang->size && isspace(lang->data[i]))
+				i++;
+
+			if (i < lang->size) {
+				size_t org = i;
+				while (i < lang->size && !isspace(lang->data[i]))
+					i++;
+
+				if (lang->data[org] == '.')
+					org++;
+
+				if (cls) bufputc(ob, ' ');
+				sdhtml_escape(ob, lang->data + org, i - org);
+			}
+		}
+
+		BUFPUTSL(ob, "\">");
+	} else
+		BUFPUTSL(ob, "<pre><code>");
+
+	if (text)
+		sdhtml_escape(ob, text->data, text->size);
+
+	BUFPUTSL(ob, "</code></pre>\n");
+}
+
+/*
+ * GitHub style code block:
+ *
+ *		<pre lang="LANG"><code>
+ *		...
+ *		</pre></code>
+ *
+ * Unlike other parsers, we store the language identifier in the <pre>,
+ * and don't let the user generate custom classes.
+ *
+ * The language identifier in the <pre> block gets postprocessed and all
+ * the code inside gets syntax highlighted with Pygments. This is much safer
+ * than letting the user specify a CSS class for highlighting.
+ *
+ * Note that we only generate HTML for the first specifier.
+ * E.g.
+ *		~~~~ {.python .numbered}	=>	<pre lang="python"><code>
+ */
+static void
+rndr_blockcode_github(struct buf *ob, struct buf *text, struct buf *lang, void *opaque)
+{
+	if (ob->size) bufputc(ob, '\n');
+
+	if (lang && lang->size) {
+		size_t i = 0;
+		BUFPUTSL(ob, "<pre lang=\"");
+
+		while (i < lang->size && !isspace(lang->data[i]))
+			i++;
+
+		if (lang->data[0] == '.')
+			sdhtml_escape(ob, lang->data + 1, i - 1);
+		else
+			sdhtml_escape(ob, lang->data, i);
+
+		BUFPUTSL(ob, "\"><code>");
+	} else
+		BUFPUTSL(ob, "<pre><code>");
+
+	if (text)
+		sdhtml_escape(ob, text->data, text->size);
+
+	BUFPUTSL(ob, "</code></pre>\n");
+}
+
+static void
+rndr_blockquote(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (ob->size) bufputc(ob, '\n');
+	BUFPUTSL(ob, "<blockquote>\n");
+	if (text) bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</blockquote>\n");
+}
+
+static int
+rndr_codespan(struct buf *ob, struct buf *text, void *opaque)
+{
+	BUFPUTSL(ob, "<code>");
+	if (text) sdhtml_escape(ob, text->data, text->size);
+	BUFPUTSL(ob, "</code>");
+	return 1;
+}
+
+static int
+rndr_strikethrough(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (!text || !text->size)
+		return 0;
+
+	BUFPUTSL(ob, "<del>");
+	bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</del>");
+	return 1;
+}
+
+static int
+rndr_double_emphasis(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (!text || !text->size)
+		return 0;
+
+	BUFPUTSL(ob, "<strong>");
+	bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</strong>");
+
+	return 1;
+}
+
+static int
+rndr_emphasis(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<em>");
+	if (text) bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</em>");
+	return 1;
+}
+
+static int
+rndr_linebreak(struct buf *ob, void *opaque)
+{
+	struct html_renderopt *options = opaque;	
+	bufputs(ob, USE_XHTML(options) ? "<br/>\n" : "<br>\n");
+	return 1;
+}
+
+static void
+rndr_header(struct buf *ob, struct buf *text, int level, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+	
+	if (ob->size)
+		bufputc(ob, '\n');
+
+	if (options->flags & HTML_TOC)
+		bufprintf(ob, "<h%d id=\"toc_%d\">", level, options->toc_data.header_count++);
+	else
+		bufprintf(ob, "<h%d>", level);
+
+	if (text) bufput(ob, text->data, text->size);
+	bufprintf(ob, "</h%d>\n", level);
+}
+
+static int
+rndr_link(struct buf *ob, struct buf *link, struct buf *title, struct buf *content, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+	
+	if (link != NULL && (options->flags & HTML_SAFELINK) != 0 && !sd_autolink_issafe(link->data, link->size))
+		return 0;
+
+	BUFPUTSL(ob, "<a href=\"");
+
+	if (link && link->size)
+		sdhtml_escape(ob, link->data, link->size);
+
+	if (title && title->size) {
+		BUFPUTSL(ob, "\" title=\"");
+		sdhtml_escape(ob, title->data, title->size);
+	}
+
+	if (options->link_attributes) {
+		bufputc(ob, '\"');
+		options->link_attributes(ob, link, opaque);
+		bufputc(ob, '>');
+	} else {
+		BUFPUTSL(ob, "\">");
+	}
+
+	if (content && content->size) bufput(ob, content->data, content->size);
+	BUFPUTSL(ob, "</a>");
+	return 1;
+}
+
+static void
+rndr_list(struct buf *ob, struct buf *text, int flags, void *opaque)
+{
+	if (ob->size) bufputc(ob, '\n');
+	bufput(ob, flags & MKD_LIST_ORDERED ? "<ol>\n" : "<ul>\n", 5);
+	if (text) bufput(ob, text->data, text->size);
+	bufput(ob, flags & MKD_LIST_ORDERED ? "</ol>\n" : "</ul>\n", 6);
+}
+
+static void
+rndr_listitem(struct buf *ob, struct buf *text, int flags, void *opaque)
+{
+	BUFPUTSL(ob, "<li>");
+	if (text) {
+		while (text->size && text->data[text->size - 1] == '\n')
+			text->size -= 1;
+		bufput(ob, text->data, text->size); }
+	BUFPUTSL(ob, "</li>\n");
+}
+
+static void
+rndr_paragraph(struct buf *ob, struct buf *text, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+	size_t i = 0;
+
+	if (ob->size) bufputc(ob, '\n');
+
+	if (!text || !text->size)
+		return;
+
+	while (i < text->size && isspace(text->data[i])) i++;
+
+	if (i == text->size)
+		return;
+
+	BUFPUTSL(ob, "<p>");
+	if (options->flags & HTML_HARD_WRAP) {
+		size_t org;
+		while (i < text->size) {
+			org = i;
+			while (i < text->size && text->data[i] != '\n')
+				i++;
+
+			if (i > org)
+				bufput(ob, text->data + org, i - org);
+
+			/*
+			 * do not insert a line break if this newline
+			 * is the last character on the paragraph
+			 */
+			if (i >= text->size - 1)
+				break;
+			
+			rndr_linebreak(ob, opaque);
+			i++;
+		}
+	} else {
+		bufput(ob, &text->data[i], text->size - i);
+	}
+	BUFPUTSL(ob, "</p>\n");
+}
+
+static void
+rndr_raw_block(struct buf *ob, struct buf *text, void *opaque)
+{
+	size_t org, sz;
+	if (!text) return;
+	sz = text->size;
+	while (sz > 0 && text->data[sz - 1] == '\n') sz -= 1;
+	org = 0;
+	while (org < sz && text->data[org] == '\n') org += 1;
+	if (org >= sz) return;
+	if (ob->size) bufputc(ob, '\n');
+	bufput(ob, text->data + org, sz - org);
+	bufputc(ob, '\n');
+}
+
+static int
+rndr_triple_emphasis(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<strong><em>");
+	bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</em></strong>");
+	return 1;
+}
+
+static void
+rndr_hrule(struct buf *ob, void *opaque)
+{
+	struct html_renderopt *options = opaque;	
+	if (ob->size) bufputc(ob, '\n');
+	bufputs(ob, USE_XHTML(options) ? "<hr/>\n" : "<hr>\n");
+}
+
+static int
+rndr_image(struct buf *ob, struct buf *link, struct buf *title, struct buf *alt, void *opaque)
+{
+	struct html_renderopt *options = opaque;	
+	if (!link || !link->size) return 0;
+	BUFPUTSL(ob, "<img src=\"");
+	sdhtml_escape(ob, link->data, link->size);
+	BUFPUTSL(ob, "\" alt=\"");
+	if (alt && alt->size)
+		sdhtml_escape(ob, alt->data, alt->size);
+	if (title && title->size) {
+		BUFPUTSL(ob, "\" title=\"");
+		sdhtml_escape(ob, title->data, title->size); }
+
+	bufputs(ob, USE_XHTML(options) ? "\"/>" : "\">");
+	return 1;
+}
+
+static int
+rndr_raw_html(struct buf *ob, struct buf *text, void *opaque)
+{
+	struct html_renderopt *options = opaque;	
+
+	if ((options->flags & HTML_SKIP_HTML) != 0)
+		return 1;
+
+	if ((options->flags & HTML_SKIP_STYLE) != 0 && sdhtml_tag(text->data, text->size, "style"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_LINKS) != 0 && sdhtml_tag(text->data, text->size, "a"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_IMAGES) != 0 && sdhtml_tag(text->data, text->size, "img"))
+		return 1;
+
+	bufput(ob, text->data, text->size);
+	return 1;
+}
+
+static void
+rndr_table(struct buf *ob, struct buf *header, struct buf *body, void *opaque)
+{
+	if (ob->size) bufputc(ob, '\n');
+	BUFPUTSL(ob, "<table><thead>\n");
+	if (header)
+		bufput(ob, header->data, header->size);
+	BUFPUTSL(ob, "</thead><tbody>\n");
+	if (body)
+		bufput(ob, body->data, body->size);
+	BUFPUTSL(ob, "</tbody></table>\n");
+}
+
+static void
+rndr_tablerow(struct buf *ob, struct buf *text, void *opaque)
+{
+	BUFPUTSL(ob, "<tr>\n");
+	if (text)
+		bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</tr>\n");
+}
+
+static void
+rndr_tablecell(struct buf *ob, struct buf *text, int flags, void *opaque)
+{
+	if (flags & MKD_TABLE_HEADER) {
+		BUFPUTSL(ob, "<th");
+	} else {
+		BUFPUTSL(ob, "<td");
+	}
+
+	switch (flags & MKD_TABLE_ALIGNMASK) {
+	case MKD_TABLE_ALIGN_CENTER:
+		BUFPUTSL(ob, " align=\"center\">");
+		break;
+
+	case MKD_TABLE_ALIGN_L:
+		BUFPUTSL(ob, " align=\"left\">");
+		break;
+
+	case MKD_TABLE_ALIGN_R:
+		BUFPUTSL(ob, " align=\"right\">");
+		break;
+
+	default:
+		BUFPUTSL(ob, ">");
+	}
+
+	if (text)
+		bufput(ob, text->data, text->size);
+
+	if (flags & MKD_TABLE_HEADER) {
+		BUFPUTSL(ob, "</th>\n");
+	} else {
+		BUFPUTSL(ob, "</td>\n");
+	}
+}
+
+static int
+rndr_superscript(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<sup>");
+	bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</sup>");
+	return 1;
+}
+
+static void
+rndr_normal_text(struct buf *ob, struct buf *text, void *opaque)
+{
+	if (text)
+		sdhtml_escape(ob, text->data, text->size);
+}
+
+static void
+toc_header(struct buf *ob, struct buf *text, int level, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	while (level > options->toc_data.current_level) {
+		if (options->toc_data.current_level > 0)
+			BUFPUTSL(ob, "<li>");
+		BUFPUTSL(ob, "<ul>\n");
+		options->toc_data.current_level++;
+	}
+
+	while (level < options->toc_data.current_level) {
+		BUFPUTSL(ob, "</ul>");
+		if (options->toc_data.current_level > 1)
+			BUFPUTSL(ob, "</li>\n");
+		options->toc_data.current_level--;
+	}
+
+	bufprintf(ob, "<li><a href=\"#toc_%d\">", options->toc_data.header_count++);
+	if (text)
+		bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</a></li>\n");
+}
+
+static void
+toc_finalize(struct buf *ob, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	while (options->toc_data.current_level > 1) {
+		BUFPUTSL(ob, "</ul></li>\n");
+		options->toc_data.current_level--;
+	}
+
+	if (options->toc_data.current_level)
+		BUFPUTSL(ob, "</ul>\n");
+}
+
+void
+sdhtml_toc_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options)
+{
+	static const struct sd_callbacks cb_default = {
+		NULL,
+		NULL,
+		NULL,
+		toc_header,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+
+		NULL,
+		rndr_codespan,
+		rndr_double_emphasis,
+		rndr_emphasis,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		rndr_triple_emphasis,
+		rndr_strikethrough,
+		rndr_superscript,
+
+		NULL,
+		NULL,
+
+		NULL,
+		toc_finalize,
+	};
+
+	memset(options, 0x0, sizeof(struct html_renderopt));
+	options->flags = HTML_TOC;
+
+	memcpy(callbacks, &cb_default, sizeof(struct sd_callbacks));
+}
+
+void
+sdhtml_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options, unsigned int render_flags)
+{
+	static const struct sd_callbacks cb_default = {
+		rndr_blockcode,
+		rndr_blockquote,
+		rndr_raw_block,
+		rndr_header,
+		rndr_hrule,
+		rndr_list,
+		rndr_listitem,
+		rndr_paragraph,
+		rndr_table,
+		rndr_tablerow,
+		rndr_tablecell,
+
+		rndr_autolink,
+		rndr_codespan,
+		rndr_double_emphasis,
+		rndr_emphasis,
+		rndr_image,
+		rndr_linebreak,
+		rndr_link,
+		rndr_raw_html,
+		rndr_triple_emphasis,
+		rndr_strikethrough,
+		rndr_superscript,
+
+		NULL,
+		rndr_normal_text,
+
+		NULL,
+		NULL,
+	};
+
+	/* Prepare the options pointer */
+	memset(options, 0x0, sizeof(struct html_renderopt));
+	options->flags = render_flags;
+
+	/* Prepare the callbacks */
+	memcpy(callbacks, &cb_default, sizeof(struct sd_callbacks));
+
+	if (render_flags & HTML_SKIP_IMAGES)
+		callbacks->image = NULL;
+
+	if (render_flags & HTML_SKIP_LINKS) {
+		callbacks->link = NULL;
+		callbacks->autolink = NULL;
+	}
+
+	if (render_flags & HTML_SKIP_HTML)
+		callbacks->blockhtml = NULL;
+
+	if (render_flags & HTML_GITHUB_BLOCKCODE)
+		callbacks->blockcode = rndr_blockcode_github;
+}
diff --git a/cbits/html.h b/cbits/html.h
new file mode 100644
--- /dev/null
+++ b/cbits/html.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UPSKIRT_HTML_H
+#define UPSKIRT_HTML_H
+
+#include "markdown.h"
+#include "buffer.h"
+#include <stdlib.h>
+
+struct html_renderopt {
+	struct {
+		int header_count;
+		int current_level;
+	} toc_data;
+
+	unsigned int flags;
+
+	/* extra callbacks */
+	void (*link_attributes)(struct buf *ob, struct buf *url, void *self);
+};
+
+typedef enum {
+	HTML_SKIP_HTML = (1 << 0),
+	HTML_SKIP_STYLE = (1 << 1),
+	HTML_SKIP_IMAGES = (1 << 2),
+	HTML_SKIP_LINKS = (1 << 3),
+	HTML_EXPAND_TABS = (1 << 5),
+	HTML_SAFELINK = (1 << 7),
+	HTML_TOC = (1 << 8),
+	HTML_HARD_WRAP = (1 << 9),
+	HTML_GITHUB_BLOCKCODE = (1 << 10),
+	HTML_USE_XHTML = (1 << 11),
+} html_render_mode;
+
+typedef enum {
+	HTML_TAG_NONE = 0,
+	HTML_TAG_OPEN,
+	HTML_TAG_CLOSE,
+} html_tag;
+
+void
+sdhtml_escape(struct buf *ob, const char *src, size_t size);
+
+int
+sdhtml_tag(const char *tag_data, size_t tag_size, const char *tagname);
+
+extern void
+sdhtml_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options_ptr, unsigned int render_flags);
+
+extern void
+sdhtml_toc_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options_ptr);
+
+extern void
+sdhtml_smartypants(struct buf *ob, struct buf *text);
+
+#endif
+
diff --git a/cbits/html_smartypants.c b/cbits/html_smartypants.c
new file mode 100644
--- /dev/null
+++ b/cbits/html_smartypants.c
@@ -0,0 +1,383 @@
+/*
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "buffer.h"
+#include "html.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+struct smartypants_data {
+	int in_squote;
+	int in_dquote;
+};
+
+static size_t smartypants_cb__ltag(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__dquote(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__amp(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__period(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__number(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__dash(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__parens(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__squote(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__backtick(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+static size_t smartypants_cb__escape(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size);
+
+static size_t (*smartypants_cb_ptrs[])
+	(struct buf *, struct smartypants_data *, char, const char *, size_t) = 
+{
+	NULL,					/* 0 */
+	smartypants_cb__dash,	/* 1 */
+	smartypants_cb__parens,	/* 2 */
+	smartypants_cb__squote, /* 3 */
+	smartypants_cb__dquote, /* 4 */
+	smartypants_cb__amp,	/* 5 */
+	smartypants_cb__period,	/* 6 */
+	smartypants_cb__number,	/* 7 */
+	smartypants_cb__ltag,	/* 8 */
+	smartypants_cb__backtick, /* 9 */
+	smartypants_cb__escape, /* 10 */
+};
+
+static const char smartypants_cb_chars[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0,
+	0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0,
+	9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static inline int
+word_boundary(char c)
+{
+	return c == 0 || isspace(c) || ispunct(c);
+}
+
+static int
+smartypants_quotes(struct buf *ob, char previous_char, char next_char, char quote, int *is_open)
+{
+	char ent[8];
+
+	if (*is_open && !word_boundary(next_char))
+		return 0;
+
+	if (!(*is_open) && !word_boundary(previous_char))
+		return 0;
+
+	snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote);
+	*is_open = !(*is_open);
+	bufputs(ob, ent);
+	return 1;
+}
+
+static size_t
+smartypants_cb__squote(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 2) {
+		char t1 = tolower(text[1]);
+
+		if (t1 == '\'') {
+			if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote))
+				return 1;
+		}
+
+		if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') &&
+			(size == 3 || word_boundary(text[2]))) {
+			BUFPUTSL(ob, "&rsquo;");
+			return 0;
+		}
+
+		if (size >= 3) {
+			char t2 = tolower(text[2]);
+
+			if (((t1 == 'r' && t2 == 'e') ||
+				(t1 == 'l' && t2 == 'l') ||
+				(t1 == 'v' && t2 == 'e')) &&
+				(size == 4 || word_boundary(text[3]))) {
+				BUFPUTSL(ob, "&rsquo;");
+				return 0;
+			}
+		}
+	}
+
+	if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote))
+		return 0;
+
+	bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__parens(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 3) {
+		char t1 = tolower(text[1]);
+		char t2 = tolower(text[2]);
+
+		if (t1 == 'c' && t2 == ')') {
+			BUFPUTSL(ob, "&copy;");
+			return 2;
+		}
+
+		if (t1 == 'r' && t2 == ')') {
+			BUFPUTSL(ob, "&reg;");
+			return 2;
+		}
+
+		if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') {
+			BUFPUTSL(ob, "&trade;");
+			return 3;
+		}
+	}
+	
+	bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__dash(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 3 && text[1] == '-' && text[2] == '-') {
+		BUFPUTSL(ob, "&mdash;");
+		return 2;
+	}
+
+	if (size >= 2 && text[1] == '-') {
+		BUFPUTSL(ob, "&ndash;");
+		return 1;
+	}
+
+	bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__amp(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 6 && memcmp(text, "&quot;", 6) == 0) {
+		if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote))
+			return 5;
+	}
+
+	if (size >= 4 && memcmp(text, "&#0;", 4) == 0)
+		return 3;
+
+	bufputc(ob, '&');
+	return 0;
+}
+
+static size_t
+smartypants_cb__period(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 3 && text[1] == '.' && text[2] == '.') {
+		BUFPUTSL(ob, "&hellip;");
+		return 2;
+	}
+
+	if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') {
+		BUFPUTSL(ob, "&hellip;");
+		return 4;
+	}
+
+	bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__backtick(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size >= 2 && text[1] == '`') {
+		if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote))
+			return 1;
+	}
+
+	return 0;
+}
+
+static size_t
+smartypants_cb__number(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (word_boundary(previous_char) && size >= 3) {
+		if (text[0] == '1' && text[1] == '/' && text[2] == '2') {
+			if (size == 3 || word_boundary(text[3])) {
+				BUFPUTSL(ob, "&frac12;");
+				return 2;
+			}
+		}
+
+		if (text[0] == '1' && text[1] == '/' && text[2] == '4') {
+			if (size == 3 || word_boundary(text[3]) ||
+				(size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) {
+				BUFPUTSL(ob, "&frac14;");
+				return 2;
+			}
+		}
+
+		if (text[0] == '3' && text[1] == '/' && text[2] == '4') {
+			if (size == 3 || word_boundary(text[3]) ||
+				(size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) {
+				BUFPUTSL(ob, "&frac34;");
+				return 2;
+			}
+		}
+	}
+
+	bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__dquote(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote))
+		BUFPUTSL(ob, "&quot;");
+
+	return 0;
+}
+
+static size_t
+smartypants_cb__ltag(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	static const char *skip_tags[] = {"pre", "code", "kbd", "script"};
+	static const size_t skip_tags_count = 4;
+
+	size_t tag, i = 0;
+
+	while (i < size && text[i] != '>')
+		i++;
+
+	for (tag = 0; tag < skip_tags_count; ++tag) {
+		if (sdhtml_tag(text, size, skip_tags[tag]) == HTML_TAG_OPEN)
+			break;
+	}
+
+	if (tag < skip_tags_count) {
+		for (;;) {
+			while (i < size && text[i] != '<')
+				i++;
+
+			if (i == size)
+				break;
+
+			if (sdhtml_tag(text + i, size - i, skip_tags[tag]) == HTML_TAG_CLOSE)
+				break;
+
+			i++;
+		}
+
+		while (i < size && text[i] != '>')
+			i++;
+	}
+
+	bufput(ob, text, i + 1);
+	return i;
+}
+
+static size_t
+smartypants_cb__escape(struct buf *ob, struct smartypants_data *smrt, char previous_char, const char *text, size_t size)
+{
+	if (size < 2)
+		return 0;
+
+	switch (text[1]) {
+	case '\\':
+	case '"':
+	case '\'':
+	case '.':
+	case '-':
+	case '`':
+		bufputc(ob, text[1]);
+		return 1;
+
+	default:
+		bufputc(ob, '\\');
+		return 0;
+	}
+}
+
+#if 0
+static struct {
+    char c0;
+    const char *pattern;
+    const char *entity;
+    int skip;
+} smartypants_subs[] = {
+    { '\'', "'s>",      "&rsquo;",  0 },
+    { '\'', "'t>",      "&rsquo;",  0 },
+    { '\'', "'re>",     "&rsquo;",  0 },
+    { '\'', "'ll>",     "&rsquo;",  0 },
+    { '\'', "'ve>",     "&rsquo;",  0 },
+    { '\'', "'m>",      "&rsquo;",  0 },
+    { '\'', "'d>",      "&rsquo;",  0 },
+    { '-',  "--",       "&mdash;",  1 },
+    { '-',  "<->",      "&ndash;",  0 },
+    { '.',  "...",      "&hellip;", 2 },
+    { '.',  ". . .",    "&hellip;", 4 },
+    { '(',  "(c)",      "&copy;",   2 },
+    { '(',  "(r)",      "&reg;",    2 },
+    { '(',  "(tm)",     "&trade;",  3 },
+    { '3',  "<3/4>",    "&frac34;", 2 },
+    { '3',  "<3/4ths>", "&frac34;", 2 },
+    { '1',  "<1/2>",    "&frac12;", 2 },
+    { '1',  "<1/4>",    "&frac14;", 2 },
+    { '1',  "<1/4th>",  "&frac14;", 2 },
+    { '&',  "&#0;",      0,       3 },
+};
+#endif
+
+void
+sdhtml_smartypants(struct buf *ob, struct buf *text)
+{
+	size_t i;
+	struct smartypants_data smrt = {0, 0};
+
+	if (!text)
+		return;
+
+	bufgrow(ob, text->size);
+
+	for (i = 0; i < text->size; ++i) {
+		size_t org;
+		char action = 0;
+
+		org = i;
+		while (i < text->size && (action = smartypants_cb_chars[(unsigned char)text->data[i]]) == 0)
+			i++;
+
+		if (i > org)
+			bufput(ob, text->data + org, i - org);
+
+		if (i < text->size) {
+			i += smartypants_cb_ptrs[(int)action]
+				(ob, &smrt, i ? text->data[i - 1] : 0, text->data + i, text->size - i);
+		}
+	}
+}
+
+
diff --git a/cbits/markdown.c b/cbits/markdown.c
new file mode 100644
--- /dev/null
+++ b/cbits/markdown.c
@@ -0,0 +1,2328 @@
+/* markdown.c - generic markdown parser */
+
+/*
+ * Copyright (c) 2009, Natacha Porté
+ * Copyright (c) 2011, Vicent Marti
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "markdown.h"
+#include "array.h"
+
+#include <assert.h>
+#include <string.h>
+//#include <strings.h> /* for strncasecmp */
+#include <ctype.h>
+#include <stdio.h>
+
+#define BUFFER_BLOCK 0
+#define BUFFER_SPAN 1
+
+#define MKD_LI_END 8	/* internal list flag */
+
+/***************
+ * LOCAL TYPES *
+ ***************/
+
+/* link_ref • reference to a link */
+struct link_ref {
+	struct buf *id;
+	struct buf *link;
+	struct buf *title;
+};
+
+/* char_trigger • function pointer to render active chars */
+/*   returns the number of chars taken care of */
+/*   data is the pointer of the beginning of the span */
+/*   offset is the number of valid chars before data */
+struct render;
+typedef size_t
+(*char_trigger)(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+
+static size_t char_emphasis(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_linebreak(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_codespan(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_escape(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_entity(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_langle_tag(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_autolink_url(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_autolink_email(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_autolink_www(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_link(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+static size_t char_superscript(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size);
+
+enum markdown_char_t {
+	MD_CHAR_NONE = 0,
+	MD_CHAR_EMPHASIS,
+	MD_CHAR_CODESPAN,
+	MD_CHAR_LINEBREAK,
+	MD_CHAR_LINK,
+	MD_CHAR_LANGLE,
+	MD_CHAR_ESCAPE,
+	MD_CHAR_ENTITITY,
+	MD_CHAR_AUTOLINK_URL,
+	MD_CHAR_AUTOLINK_EMAIL,
+	MD_CHAR_AUTOLINK_WWW,
+	MD_CHAR_SUPERSCRIPT,
+};
+
+static char_trigger markdown_char_ptrs[] = {
+	NULL,
+	&char_emphasis,
+	&char_codespan,
+	&char_linebreak,
+	&char_link,
+	&char_langle_tag,
+	&char_escape,
+	&char_entity,
+	&char_autolink_url,
+	&char_autolink_email,
+	&char_autolink_www,
+	&char_superscript,
+};
+
+/* render • structure containing one particular render */
+struct render {
+	struct sd_callbacks	cb;
+	void *opaque;
+
+	struct array refs;
+	char active_char[256];
+	struct parray work_bufs[2];
+	unsigned int ext_flags;
+	size_t max_nesting;
+};
+
+/* html_tag • structure for quick HTML tag search (inspired from discount) */
+struct html_tag {
+	const char *text;
+	size_t size;
+};
+
+static inline struct buf *
+rndr_newbuf(struct render *rndr, int type)
+{
+	static const size_t buf_size[2] = {256, 64};
+	struct buf *work = NULL;
+	struct parray *queue = &rndr->work_bufs[type];
+
+	if (queue->size < queue->asize) {
+		work = queue->item[queue->size++];
+		work->size = 0;
+	} else {
+		work = bufnew(buf_size[type]);
+		parr_push(queue, work);
+	}
+
+	return work;
+}
+
+static inline void
+rndr_popbuf(struct render *rndr, int type)
+{
+	rndr->work_bufs[type].size--;
+}
+
+/********************
+ * GLOBAL VARIABLES *
+ ********************/
+
+/* block_tags • recognised block tags, sorted by cmp_html_tag */
+static struct html_tag block_tags[] = {
+/*0*/	{ "p",		1 },
+	{ "dl",		2 },
+	{ "h1",		2 },
+	{ "h2",		2 },
+	{ "h3",		2 },
+	{ "h4",		2 },
+	{ "h5",		2 },
+	{ "h6",		2 },
+	{ "ol",		2 },
+	{ "ul",		2 },
+	{ "del",	3 }, /* 10 */
+	{ "div",	3 },
+	{ "ins",	3 }, /* 12 */
+	{ "pre",	3 },
+	{ "form",	4 },
+	{ "math",	4 },
+	{ "table",	5 },
+	{ "figure",	6 },
+	{ "iframe",	6 },
+	{ "script",	6 },
+	{ "fieldset",	8 },
+	{ "noscript",	8 },
+	{ "blockquote",	10 }
+};
+
+#define INS_TAG (block_tags + 12)
+#define DEL_TAG (block_tags + 10)
+
+/***************************
+ * HELPER FUNCTIONS *
+ ***************************/
+static void
+unscape_text(struct buf *ob, struct buf *src)
+{
+	size_t i = 0, org;
+	while (i < src->size) {
+		org = i;
+		while (i < src->size && src->data[i] != '\\')
+			i++;
+
+		if (i > org)
+			bufput(ob, src->data + org, i - org);
+
+		if (i + 1 >= src->size)
+			break;
+
+		bufputc(ob, src->data[i + 1]);
+		i += 2;
+	}
+}
+
+/* cmp_link_ref • comparison function for link_ref sorted arrays */
+static int
+cmp_link_ref(void *key, void *array_entry)
+{
+	struct link_ref *lr = array_entry;
+	return bufcasecmp(key, lr->id);
+}
+
+/* cmp_link_ref_sort • comparison function for link_ref qsort */
+static int
+cmp_link_ref_sort(const void *a, const void *b)
+{
+	const struct link_ref *lra = a;
+	const struct link_ref *lrb = b;
+	return bufcasecmp(lra->id, lrb->id);
+}
+
+/* cmp_html_tag • comparison function for bsearch() (stolen from discount) */
+static int
+cmp_html_tag(const void *a, const void *b)
+{
+	const struct html_tag *hta = a;
+	const struct html_tag *htb = b;
+	if (hta->size != htb->size) return (int)(hta->size - htb->size);
+	return strncasecmp(hta->text, htb->text, hta->size);
+}
+
+
+/* find_block_tag • returns the current block tag */
+static struct html_tag *
+find_block_tag(char *data, size_t size)
+{
+	size_t i = 0;
+	struct html_tag key;
+
+	/* looking for the word end */
+	while (i < size && ((data[i] >= '0' && data[i] <= '9')
+				|| (data[i] >= 'A' && data[i] <= 'Z')
+				|| (data[i] >= 'a' && data[i] <= 'z')))
+		i++;
+	if (i >= size) return 0;
+
+	/* binary search of the tag */
+	key.text = data;
+	key.size = i;
+	return bsearch(&key, block_tags,
+				sizeof block_tags / sizeof block_tags[0],
+				sizeof block_tags[0], cmp_html_tag);
+}
+
+/****************************
+ * INLINE PARSING FUNCTIONS *
+ ****************************/
+
+/* is_mail_autolink • looks for the address part of a mail autolink and '>' */
+/* this is less strict than the original markdown e-mail address matching */
+static size_t
+is_mail_autolink(char *data, size_t size)
+{
+	size_t i = 0, nb = 0;
+
+	/* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
+	for (i = 0; i < size; ++i) {
+		if (isalnum(data[i]))
+			continue;
+
+		switch (data[i]) {
+			case '@':
+				nb++;
+
+			case '-':
+			case '.':
+			case '_':
+				break;
+
+			case '>':
+				return (nb == 1) ? i + 1 : 0;
+
+			default:
+				return 0;
+		}
+	}
+
+	return 0;
+}
+
+/* tag_length • returns the length of the given tag, or 0 is it's not valid */
+static size_t
+tag_length(char *data, size_t size, enum mkd_autolink *autolink)
+{
+	size_t i, j;
+
+	/* a valid tag can't be shorter than 3 chars */
+	if (size < 3) return 0;
+
+	/* begins with a '<' optionally followed by '/', followed by letter or number */
+	if (data[0] != '<') return 0;
+	i = (data[1] == '/') ? 2 : 1;
+
+	if (!isalnum(data[i]))
+		return 0;
+
+	/* scheme test */
+	*autolink = MKDA_NOT_AUTOLINK;
+
+	/* try to find the beginning of an URI */
+	while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-'))
+		i++;
+
+	if (i > 1 && data[i] == '@') {
+		if ((j = is_mail_autolink(data + i, size - i)) != 0) {
+			*autolink = MKDA_EMAIL;
+			return i + j;
+		}
+	}
+
+	if (i > 2 && data[i] == ':') {
+		*autolink = MKDA_NORMAL;
+		i++;
+	}
+
+	/* completing autolink test: no whitespace or ' or " */
+	if (i >= size)
+		*autolink = MKDA_NOT_AUTOLINK;
+
+	else if (*autolink) {
+		j = i;
+
+		while (i < size) {
+			if (data[i] == '\\') i += 2;
+			else if (data[i] == '>' || data[i] == '\'' ||
+					data[i] == '"' || isspace(data[i])) break;
+			else i++;
+		}
+
+		if (i >= size) return 0;
+		if (i > j && data[i] == '>') return i + 1;
+		/* one of the forbidden chars has been found */
+		*autolink = MKDA_NOT_AUTOLINK;
+	}
+
+	/* looking for sometinhg looking like a tag end */
+	while (i < size && data[i] != '>') i++;
+	if (i >= size) return 0;
+	return i + 1;
+}
+
+/* parse_inline • parses inline markdown elements */
+static void
+parse_inline(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t i = 0, end = 0;
+	char action = 0;
+	struct buf work = { 0, 0, 0, 0, 0 };
+
+	if (rndr->work_bufs[BUFFER_SPAN].size +
+		rndr->work_bufs[BUFFER_BLOCK].size > (int)rndr->max_nesting)
+		return;
+
+	while (i < size) {
+		/* copying inactive chars into the output */
+		while (end < size && (action = rndr->active_char[(unsigned char)data[end]]) == 0) {
+			end++;
+		}
+
+		if (rndr->cb.normal_text) {
+			work.data = data + i;
+			work.size = end - i;
+			rndr->cb.normal_text(ob, &work, rndr->opaque);
+		}
+		else
+			bufput(ob, data + i, end - i);
+
+		if (end >= size) break;
+		i = end;
+
+		/* calling the trigger */
+		end = markdown_char_ptrs[(int)action](ob, rndr, data + i, i, size - i);
+		if (!end) /* no action from the callback */
+			end = i + 1;
+		else { 
+			i += end;
+			end = i;
+		} 
+	}
+}
+
+/* find_emph_char • looks for the next emph char, skipping other constructs */
+static size_t
+find_emph_char(char *data, size_t size, char c)
+{
+	size_t i = 1;
+
+	while (i < size) {
+		while (i < size && data[i] != c && data[i] != '`' && data[i] != '[')
+			i++;
+
+		if (i == size)
+			return 0;
+
+		if (data[i] == c)
+			return i;
+
+		/* not counting escaped chars */
+		if (i && data[i - 1] == '\\') {
+			i++; continue;
+		}
+
+		if (data[i] == '`') {
+			size_t span_nb = 0, bt;
+			size_t tmp_i = 0;
+
+			/* counting the number of opening backticks */
+			while (i < size && data[i] == '`') {
+				i++; span_nb++;
+			}
+
+			if (i >= size) return 0;
+
+			/* finding the matching closing sequence */
+			bt = 0;
+			while (i < size && bt < span_nb) {
+				if (!tmp_i && data[i] == c) tmp_i = i;
+				if (data[i] == '`') bt++;
+				else bt = 0;
+				i++;
+			}
+
+			if (i >= size) return tmp_i;
+			i++;
+		}
+		/* skipping a link */
+		else if (data[i] == '[') {
+			size_t tmp_i = 0;
+			char cc;
+
+			i++;
+			while (i < size && data[i] != ']') {
+				if (!tmp_i && data[i] == c) tmp_i = i;
+				i++;
+			}
+
+			i++;
+			while (i < size && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n'))
+				i++;
+
+			if (i >= size)
+				return tmp_i;
+
+			if (data[i] != '[' && data[i] != '(') { /* not a link*/
+				if (tmp_i) return tmp_i;
+				else continue;
+			}
+
+			cc = data[i];
+			i++;
+			while (i < size && data[i] != cc) {
+				if (!tmp_i && data[i] == c) tmp_i = i;
+				i++;
+			}
+
+			if (i >= size)
+				return tmp_i;
+
+			i++;
+		}
+	}
+
+	return 0;
+}
+
+/* parse_emph1 • parsing single emphase */
+/* closed by a symbol not preceded by whitespace and not followed by symbol */
+static size_t
+parse_emph1(struct buf *ob, struct render *rndr, char *data, size_t size, char c)
+{
+	size_t i = 0, len;
+	struct buf *work = 0;
+	int r;
+
+	if (!rndr->cb.emphasis) return 0;
+
+	/* skipping one symbol if coming from emph3 */
+	if (size > 1 && data[0] == c && data[1] == c) i = 1;
+
+	while (i < size) {
+		len = find_emph_char(data + i, size - i, c);
+		if (!len) return 0;
+		i += len;
+		if (i >= size) return 0;
+
+		if (i + 1 < size && data[i + 1] == c) {
+			i++;
+			continue;
+		}
+
+		if (data[i] == c && !isspace(data[i - 1])) {
+
+			if (rndr->ext_flags & MKDEXT_NO_INTRA_EMPHASIS) {
+				if (!(i + 1 == size || isspace(data[i + 1]) || ispunct(data[i + 1])))
+					continue;
+			}
+
+			work = rndr_newbuf(rndr, BUFFER_SPAN);
+			parse_inline(work, rndr, data, i);
+			r = rndr->cb.emphasis(ob, work, rndr->opaque);
+			rndr_popbuf(rndr, BUFFER_SPAN);
+			return r ? i + 1 : 0;
+		}
+	}
+
+	return 0;
+}
+
+/* parse_emph2 • parsing single emphase */
+static size_t
+parse_emph2(struct buf *ob, struct render *rndr, char *data, size_t size, char c)
+{
+	int (*render_method)(struct buf *ob, struct buf *text, void *opaque);
+	size_t i = 0, len;
+	struct buf *work = 0;
+	int r;
+
+	render_method = (c == '~') ? rndr->cb.strikethrough : rndr->cb.double_emphasis;
+
+	if (!render_method)
+		return 0;
+	
+	while (i < size) {
+		len = find_emph_char(data + i, size - i, c);
+		if (!len) return 0;
+		i += len;
+
+		if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !isspace(data[i - 1])) {
+			work = rndr_newbuf(rndr, BUFFER_SPAN);
+			parse_inline(work, rndr, data, i);
+			r = render_method(ob, work, rndr->opaque);
+			rndr_popbuf(rndr, BUFFER_SPAN);
+			return r ? i + 2 : 0;
+		}
+		i++;
+	}
+	return 0;
+}
+
+/* parse_emph3 • parsing single emphase */
+/* finds the first closing tag, and delegates to the other emph */
+static size_t
+parse_emph3(struct buf *ob, struct render *rndr, char *data, size_t size, char c)
+{
+	size_t i = 0, len;
+	int r;
+
+	while (i < size) {
+		len = find_emph_char(data + i, size - i, c);
+		if (!len) return 0;
+		i += len;
+
+		/* skip whitespace preceded symbols */
+		if (data[i] != c || isspace(data[i - 1]))
+			continue;
+
+		if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && rndr->cb.triple_emphasis) {
+			/* triple symbol found */
+			struct buf *work = rndr_newbuf(rndr, BUFFER_SPAN);
+
+			parse_inline(work, rndr, data, i);
+			r = rndr->cb.triple_emphasis(ob, work, rndr->opaque);
+			rndr_popbuf(rndr, BUFFER_SPAN);
+			return r ? i + 3 : 0;
+
+		} else if (i + 1 < size && data[i + 1] == c) {
+			/* double symbol found, handing over to emph1 */
+			len = parse_emph1(ob, rndr, data - 2, size + 2, c);
+			if (!len) return 0;
+			else return len - 2;
+
+		} else {
+			/* single symbol found, handing over to emph2 */
+			len = parse_emph2(ob, rndr, data - 1, size + 1, c);
+			if (!len) return 0;
+			else return len - 1;
+		}
+	}
+	return 0; 
+}
+
+/* char_emphasis • single and double emphasis parsing */
+static size_t
+char_emphasis(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	char c = data[0];
+	size_t ret;
+
+	if (size > 2 && data[1] != c) {
+		/* whitespace cannot follow an opening emphasis;
+		 * strikethrough only takes two characters '~~' */
+		if (c == '~' || isspace(data[1]) || (ret = parse_emph1(ob, rndr, data + 1, size - 1, c)) == 0)
+			return 0;
+
+		return ret + 1;
+	}
+
+	if (size > 3 && data[1] == c && data[2] != c) {
+		if (isspace(data[2]) || (ret = parse_emph2(ob, rndr, data + 2, size - 2, c)) == 0)
+			return 0;
+
+		return ret + 2;
+	}
+
+	if (size > 4 && data[1] == c && data[2] == c && data[3] != c) {
+		if (c == '~' || isspace(data[3]) || (ret = parse_emph3(ob, rndr, data + 3, size - 3, c)) == 0)
+			return 0;
+
+		return ret + 3;
+	}
+
+	return 0; 
+}
+
+
+/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */
+static size_t
+char_linebreak(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	if (offset < 2 || data[-1] != ' ' || data[-2] != ' ')
+		return 0;
+
+	/* removing the last space from ob and rendering */
+	while (ob->size && ob->data[ob->size - 1] == ' ')
+		ob->size--;
+
+	return rndr->cb.linebreak(ob, rndr->opaque) ? 1 : 0;
+}
+
+
+/* char_codespan • '`' parsing a code span (assuming codespan != 0) */
+static size_t
+char_codespan(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	size_t end, nb = 0, i, f_begin, f_end;
+
+	/* counting the number of backticks in the delimiter */
+	while (nb < size && data[nb] == '`')
+		nb++;
+
+	/* finding the next delimiter */
+	i = 0;
+	for (end = nb; end < size && i < nb; end++) {
+		if (data[end] == '`') i++;
+		else i = 0;
+	}
+
+	if (i < nb && end >= size)
+		return 0; /* no matching delimiter */
+
+	/* trimming outside whitespaces */
+	f_begin = nb;
+	while (f_begin < end && (data[f_begin] == ' ' || data[f_begin] == '\t'))
+		f_begin++;
+
+	f_end = end - nb;
+	while (f_end > nb && (data[f_end-1] == ' ' || data[f_end-1] == '\t'))
+		f_end--;
+
+	/* real code span */
+	if (f_begin < f_end) {
+		struct buf work = { data + f_begin, f_end - f_begin, 0, 0, 0 };
+		if (!rndr->cb.codespan(ob, &work, rndr->opaque))
+			end = 0;
+	} else {
+		if (!rndr->cb.codespan(ob, 0, rndr->opaque))
+			end = 0;
+	}
+
+	return end;
+}
+
+
+/* char_escape • '\\' backslash escape */
+static size_t
+char_escape(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>";
+	struct buf work = { 0, 0, 0, 0, 0 };
+
+	if (size > 1) {
+		if (strchr(escape_chars, data[1]) == NULL)
+			return 0;
+
+		if (rndr->cb.normal_text) {
+			work.data = data + 1;
+			work.size = 1;
+			rndr->cb.normal_text(ob, &work, rndr->opaque);
+		}
+		else bufputc(ob, data[1]);
+	}
+
+	return 2;
+}
+
+/* char_entity • '&' escaped when it doesn't belong to an entity */
+/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */
+static size_t
+char_entity(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	size_t end = 1;
+	struct buf work;
+
+	if (end < size && data[end] == '#')
+		end++;
+
+	while (end < size && isalnum(data[end]))
+		end++;
+
+	if (end < size && data[end] == ';')
+		end++; /* real entity */
+	else
+		return 0; /* lone '&' */
+
+	if (rndr->cb.entity) {
+		work.data = data;
+		work.size = end;
+		rndr->cb.entity(ob, &work, rndr->opaque);
+	}
+	else bufput(ob, data, end);
+
+	return end;
+}
+
+/* char_langle_tag • '<' when tags or autolinks are allowed */
+static size_t
+char_langle_tag(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	enum mkd_autolink altype = MKDA_NOT_AUTOLINK;
+	size_t end = tag_length(data, size, &altype);
+	struct buf work = { data, end, 0, 0, 0 };
+	int ret = 0;
+
+	if (end > 2) {
+		if (rndr->cb.autolink && altype != MKDA_NOT_AUTOLINK) {
+			struct buf *u_link = rndr_newbuf(rndr, BUFFER_SPAN);
+			work.data = data + 1;
+			work.size = end - 2;
+			unscape_text(u_link, &work);
+			ret = rndr->cb.autolink(ob, u_link, altype, rndr->opaque);
+			rndr_popbuf(rndr, BUFFER_SPAN);
+		}
+		else if (rndr->cb.raw_html_tag)
+			ret = rndr->cb.raw_html_tag(ob, &work, rndr->opaque);
+	}
+
+	if (!ret) return 0;
+	else return end;
+}
+
+static size_t
+char_autolink_www(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	struct buf *link, *link_url;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.link)
+		return 0;
+
+	link = rndr_newbuf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__www(&rewind, link, data, offset, size)) > 0) {
+		link_url = rndr_newbuf(rndr, BUFFER_SPAN);
+		BUFPUTSL(link_url, "http://");
+		bufput(link_url, link->data, link->size);
+
+		ob->size -= rewind;
+		rndr->cb.link(ob, link_url, NULL, link, rndr->opaque);
+		rndr_popbuf(rndr, BUFFER_SPAN);
+	}
+
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+static size_t
+char_autolink_email(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	struct buf *link;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.autolink)
+		return 0;
+
+	link = rndr_newbuf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__email(&rewind, link, data, offset, size)) > 0) {
+		ob->size -= rewind;
+		rndr->cb.autolink(ob, link, MKDA_EMAIL, rndr->opaque);
+	}
+
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+static size_t
+char_autolink_url(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	struct buf *link;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.autolink)
+		return 0;
+
+	link = rndr_newbuf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__url(&rewind, link, data, offset, size)) > 0) {
+		ob->size -= rewind;
+		rndr->cb.autolink(ob, link, MKDA_NORMAL, rndr->opaque);
+	}
+
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+/* char_link • '[': parsing a link or an image */
+static size_t
+char_link(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	int is_img = (offset && data[-1] == '!'), level;
+	size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
+	struct buf *content = 0;
+	struct buf *link = 0;
+	struct buf *title = 0;
+	struct buf *u_link = 0;
+	size_t org_work_size = rndr->work_bufs[BUFFER_SPAN].size;
+	int text_has_nl = 0, ret = 0;
+
+	/* checking whether the correct renderer exists */
+	if ((is_img && !rndr->cb.image) || (!is_img && !rndr->cb.link))
+		goto cleanup;
+
+	/* looking for the matching closing bracket */
+	for (level = 1; i < size; i++) {
+		if (data[i] == '\n')
+			text_has_nl = 1;
+
+		else if (data[i - 1] == '\\')
+			continue;
+
+		else if (data[i] == '[')
+			level++;
+
+		else if (data[i] == ']') {
+			level--;
+			if (level <= 0)
+				break;
+		}
+	}
+
+	if (i >= size)
+		goto cleanup;
+
+	txt_e = i;
+	i++;
+
+	/* skip any amount of whitespace or newline */
+	/* (this is much more laxist than original markdown syntax) */
+	while (i < size && isspace(data[i]))
+		i++;
+
+	/* inline style link */
+	if (i < size && data[i] == '(') {
+		/* skipping initial whitespace */
+		i++;
+
+		while (i < size && isspace(data[i]))
+			i++;
+
+		link_b = i;
+
+		/* looking for link end: ' " ) */
+		while (i < size) {
+			if (data[i] == '\\') i += 2;
+			else if (data[i] == ')' || data[i] == '\'' || data[i] == '"') break;
+			else i++;
+		}
+
+		if (i >= size) goto cleanup;
+		link_e = i;
+
+		/* looking for title end if present */
+		if (data[i] == '\'' || data[i] == '"') {
+			i++;
+			title_b = i;
+
+			while (i < size) {
+				if (data[i] == '\\') i += 2;
+				else if (data[i] == ')') break;
+				else i++;
+			}
+
+			if (i >= size) goto cleanup;
+
+			/* skipping whitespaces after title */
+			title_e = i - 1;
+			while (title_e > title_b && isspace(data[title_e]))
+				title_e--;
+
+			/* checking for closing quote presence */
+			if (data[title_e] != '\'' &&  data[title_e] != '"') {
+				title_b = title_e = 0;
+				link_e = i;
+			}
+		}
+
+		/* remove whitespace at the end of the link */
+		while (link_e > link_b && isspace(data[link_e - 1]))
+			link_e--;
+
+		/* remove optional angle brackets around the link */
+		if (data[link_b] == '<') link_b++;
+		if (data[link_e - 1] == '>') link_e--;
+
+		/* building escaped link and title */
+		if (link_e > link_b) {
+			link = rndr_newbuf(rndr, BUFFER_SPAN);
+			bufput(link, data + link_b, link_e - link_b);
+		}
+
+		if (title_e > title_b) {
+			title = rndr_newbuf(rndr, BUFFER_SPAN);
+			bufput(title, data + title_b, title_e - title_b);
+		}
+
+		i++;
+	}
+
+	/* reference style link */
+	else if (i < size && data[i] == '[') {
+		struct buf id = { 0, 0, 0, 0, 0 };
+		struct link_ref *lr;
+
+		/* looking for the id */
+		i++;
+		link_b = i;
+		while (i < size && data[i] != ']') i++;
+		if (i >= size) goto cleanup;
+		link_e = i;
+
+		/* finding the link_ref */
+		if (link_b == link_e) {
+			if (text_has_nl) {
+				struct buf *b = rndr_newbuf(rndr, BUFFER_SPAN);
+				size_t j;
+
+				for (j = 1; j < txt_e; j++) {
+					if (data[j] != '\n')
+						bufputc(b, data[j]);
+					else if (data[j - 1] != ' ')
+						bufputc(b, ' ');
+				}
+
+				id.data = b->data;
+				id.size = b->size;
+			} else {
+				id.data = data + 1;
+				id.size = txt_e - 1;
+			}
+		} else {
+			id.data = data + link_b;
+			id.size = link_e - link_b;
+		}
+
+		lr = arr_sorted_find(&rndr->refs, &id, cmp_link_ref);
+		if (!lr) goto cleanup;
+
+		/* keeping link and title from link_ref */
+		link = lr->link;
+		title = lr->title;
+		i++;
+	}
+
+	/* shortcut reference style link */
+	else {
+		struct buf id = { 0, 0, 0, 0, 0 };
+		struct link_ref *lr;
+
+		/* crafting the id */
+		if (text_has_nl) {
+			struct buf *b = rndr_newbuf(rndr, BUFFER_SPAN);
+			size_t j;
+
+			for (j = 1; j < txt_e; j++) {
+				if (data[j] != '\n')
+					bufputc(b, data[j]);
+				else if (data[j - 1] != ' ')
+					bufputc(b, ' ');
+			}
+
+			id.data = b->data;
+			id.size = b->size;
+		} else {
+			id.data = data + 1;
+			id.size = txt_e - 1;
+		}
+
+		/* finding the link_ref */
+		lr = arr_sorted_find(&rndr->refs, &id, cmp_link_ref);
+		if (!lr) goto cleanup;
+
+		/* keeping link and title from link_ref */
+		link = lr->link;
+		title = lr->title;
+
+		/* rewinding the whitespace */
+		i = txt_e + 1;
+	}
+
+	/* building content: img alt is escaped, link content is parsed */
+	if (txt_e > 1) {
+		content = rndr_newbuf(rndr, BUFFER_SPAN);
+		if (is_img) bufput(content, data + 1, txt_e - 1);
+		else parse_inline(content, rndr, data + 1, txt_e - 1);
+	}
+
+	if (link) {
+		u_link = rndr_newbuf(rndr, BUFFER_SPAN);
+		unscape_text(u_link, link);
+	}
+
+	/* calling the relevant rendering function */
+	if (is_img) {
+		if (ob->size && ob->data[ob->size - 1] == '!')
+			ob->size -= 1;
+
+		ret = rndr->cb.image(ob, u_link, title, content, rndr->opaque);
+	} else {
+		ret = rndr->cb.link(ob, u_link, title, content, rndr->opaque);
+	}
+
+	/* cleanup */
+cleanup:
+	rndr->work_bufs[BUFFER_SPAN].size = (int)org_work_size;
+	return ret ? i : 0;
+}
+
+static size_t
+char_superscript(struct buf *ob, struct render *rndr, char *data, size_t offset, size_t size)
+{
+	size_t sup_start, sup_len;
+	struct buf *sup;
+
+	if (!rndr->cb.superscript)
+		return 0;
+
+	if (size < 2)
+		return 0;
+
+	if (data[1] == '(') {
+		sup_start = sup_len = 2;
+
+		while (sup_len < size && data[sup_len] != ')' && data[sup_len - 1] != '\\')
+			sup_len++;
+
+		if (sup_len == size)
+			return 0;
+	} else {
+		sup_start = sup_len = 1;
+
+		while (sup_len < size && !isspace(data[sup_len]))
+			sup_len++;
+	}
+
+	if (sup_len - sup_start == 0)
+		return (sup_start == 2) ? 3 : 0;
+
+	sup = rndr_newbuf(rndr, BUFFER_SPAN);
+	parse_inline(sup, rndr, data + sup_start, sup_len - sup_start);
+	rndr->cb.superscript(ob, sup, rndr->opaque);
+	rndr_popbuf(rndr, BUFFER_SPAN);
+
+	return (sup_start == 2) ? sup_len + 1 : sup_len;
+}
+
+/*********************************
+ * BLOCK-LEVEL PARSING FUNCTIONS *
+ *********************************/
+
+/* is_empty • returns the line length when it is empty, 0 otherwise */
+static size_t
+is_empty(char *data, size_t size)
+{
+	size_t i;
+	for (i = 0; i < size && data[i] != '\n'; i++)
+		if (data[i] != ' ' && data[i] != '\t') return 0;
+	return i + 1;
+}
+
+/* is_hrule • returns whether a line is a horizontal rule */
+static int
+is_hrule(char *data, size_t size)
+{
+	size_t i = 0, n = 0;
+	char c;
+
+	/* skipping initial spaces */
+	if (size < 3) return 0;
+	if (data[0] == ' ') { i++;
+	if (data[1] == ' ') { i++;
+	if (data[2] == ' ') { i++; } } }
+
+	/* looking at the hrule char */
+	if (i + 2 >= size
+	|| (data[i] != '*' && data[i] != '-' && data[i] != '_'))
+		return 0;
+	c = data[i];
+
+	/* the whole line must be the char or whitespace */
+	while (i < size && data[i] != '\n') {
+		if (data[i] == c) n++;
+		else if (data[i] != ' ' && data[i] != '\t')
+			return 0;
+		i++; }
+
+	return n >= 3;
+}
+
+/* check if a line is a code fence; return its size if it is */
+static size_t
+is_codefence(char *data, size_t size, struct buf *syntax)
+{
+	size_t i = 0, n = 0;
+	char c;
+
+	/* skipping initial spaces */
+	if (size < 3) return 0;
+	if (data[0] == ' ') { i++;
+	if (data[1] == ' ') { i++;
+	if (data[2] == ' ') { i++; } } }
+
+	/* looking at the hrule char */
+	if (i + 2 >= size || !(data[i] == '~' || data[i] == '`'))
+		return 0;
+
+	c = data[i];
+
+	/* the whole line must be the char or whitespace */
+	while (i < size && data[i] == c) {
+		n++; i++;
+	}
+
+	if (n < 3)
+		return 0;
+
+	if (syntax != NULL) {
+		size_t syn = 0;
+
+		while (i < size && (data[i] == ' ' || data[i] == '\t'))
+			i++;
+
+		syntax->data = data + i;
+
+		if (i < size && data[i] == '{') {
+			i++; syntax->data++;
+
+			while (i < size && data[i] != '}' && data[i] != '\n') {
+				syn++; i++;
+			}
+
+			if (i == size || data[i] != '}')
+				return 0;
+
+			/* strip all whitespace at the beginning and the end
+			 * of the {} block */
+			while (syn > 0 && isspace(syntax->data[0])) {
+				syntax->data++; syn--;
+			}
+
+			while (syn > 0 && isspace(syntax->data[syn - 1]))
+				syn--;
+
+			i++;
+		} else {
+			while (i < size && !isspace(data[i])) {
+				syn++; i++;
+			}
+		}
+
+		syntax->size = syn;
+	}
+
+	while (i < size && data[i] != '\n') {
+		if (!isspace(data[i]))
+			return 0;
+
+		i++;
+	}
+
+	return i + 1;
+}
+
+/* is_atxheader • returns whether the line is a hash-prefixed header */
+static int
+is_atxheader(struct render *rndr, char *data, size_t size)
+{
+	if (data[0] != '#')
+		return 0;
+
+	if (rndr->ext_flags & MKDEXT_SPACE_HEADERS) {
+		size_t level = 0;
+
+		while (level < size && level < 6 && data[level] == '#')
+			level++;
+
+		if (level < size && data[level] != ' ' && data[level] != '\t')
+			return 0;
+	}
+
+	return 1;
+}
+
+/* is_headerline • returns whether the line is a setext-style hdr underline */
+static int
+is_headerline(char *data, size_t size)
+{
+	size_t i = 0;
+
+	/* test of level 1 header */
+	if (data[i] == '=') {
+		for (i = 1; i < size && data[i] == '='; i++);
+		while (i < size && (data[i] == ' ' || data[i] == '\t')) i++;
+		return (i >= size || data[i] == '\n') ? 1 : 0; }
+
+	/* test of level 2 header */
+	if (data[i] == '-') {
+		for (i = 1; i < size && data[i] == '-'; i++);
+		while (i < size && (data[i] == ' ' || data[i] == '\t')) i++;
+		return (i >= size || data[i] == '\n') ? 2 : 0; }
+
+	return 0;
+}
+
+/* prefix_quote • returns blockquote prefix length */
+static size_t
+prefix_quote(char *data, size_t size)
+{
+	size_t i = 0;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == '>') {
+		if (i + 1 < size && (data[i + 1] == ' ' || data[i+1] == '\t'))
+			return i + 2;
+		else return i + 1; }
+	else return 0;
+}
+
+/* prefix_code • returns prefix length for block code*/
+static size_t
+prefix_code(char *data, size_t size)
+{
+	if (size > 0 && data[0] == '\t') return 1;
+	if (size > 3 && data[0] == ' ' && data[1] == ' '
+			&& data[2] == ' ' && data[3] == ' ') return 4;
+	return 0;
+}
+
+/* prefix_oli • returns ordered list item prefix */
+static size_t
+prefix_oli(char *data, size_t size)
+{
+	size_t i = 0;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i >= size || data[i] < '0' || data[i] > '9') return 0;
+	while (i < size && data[i] >= '0' && data[i] <= '9') i++;
+	if (i + 1 >= size || data[i] != '.'
+	|| (data[i + 1] != ' ' && data[i + 1] != '\t')) return 0;
+	return i + 2;
+}
+
+/* prefix_uli • returns ordered list item prefix */
+static size_t
+prefix_uli(char *data, size_t size)
+{
+	size_t i = 0;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i < size && data[i] == ' ') i++;
+	if (i + 1 >= size
+	|| (data[i] != '*' && data[i] != '+' && data[i] != '-')
+	|| (data[i + 1] != ' ' && data[i + 1] != '\t'))
+		return 0;
+	return i + 2;
+}
+
+
+/* parse_block • parsing of one block, returning next char to parse */
+static void parse_block(struct buf *ob, struct render *rndr,
+			char *data, size_t size);
+
+
+/* parse_blockquote • handles parsing of a blockquote fragment */
+static size_t
+parse_blockquote(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t beg, end = 0, pre, work_size = 0;
+	char *work_data = 0;
+	struct buf *out = 0;
+
+	out = rndr_newbuf(rndr, BUFFER_BLOCK);
+	beg = 0;
+	while (beg < size) {
+		for (end = beg + 1; end < size && data[end - 1] != '\n'; end++);
+
+		pre = prefix_quote(data + beg, end - beg);
+
+		if (pre)
+			beg += pre; /* skipping prefix */
+
+		/* empty line followed by non-quote line */
+		else if (is_empty(data + beg, end - beg) &&
+				(end >= size || (prefix_quote(data + end, size - end) == 0 &&
+				!is_empty(data + end, size - end))))
+			break;
+
+		if (beg < end) { /* copy into the in-place working buffer */
+			/* bufput(work, data + beg, end - beg); */
+			if (!work_data)
+				work_data = data + beg;
+			else if (data + beg != work_data + work_size)
+				memmove(work_data + work_size, data + beg, end - beg);
+			work_size += end - beg;
+		}
+		beg = end;
+	}
+
+	parse_block(out, rndr, work_data, work_size);
+	if (rndr->cb.blockquote)
+		rndr->cb.blockquote(ob, out, rndr->opaque);
+	rndr_popbuf(rndr, BUFFER_BLOCK);
+	return end;
+}
+
+static size_t
+parse_htmlblock(struct buf *ob, struct render *rndr, char *data, size_t size, int do_render);
+
+/* parse_blockquote • handles parsing of a regular paragraph */
+static size_t
+parse_paragraph(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t i = 0, end = 0;
+	int level = 0;
+	struct buf work = { data, 0, 0, 0, 0 }; /* volatile working buffer */
+
+	while (i < size) {
+		for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */;
+
+		if (is_empty(data + i, size - i) || (level = is_headerline(data + i, size - i)) != 0)
+			break;
+
+		if (rndr->ext_flags & MKDEXT_LAX_HTML_BLOCKS) {
+			if (data[i] == '<' && rndr->cb.blockhtml && parse_htmlblock(ob, rndr, data + i, size - i, 0)) {
+				end = i;
+				break;
+			}
+		}
+
+		if (is_atxheader(rndr, data + i, size - i) || is_hrule(data + i, size - i)) {
+			end = i;
+			break;
+		}
+
+		i = end;
+	}
+
+	work.size = i;
+	while (work.size && data[work.size - 1] == '\n')
+		work.size--;
+
+	if (!level) {
+		struct buf *tmp = rndr_newbuf(rndr, BUFFER_BLOCK);
+		parse_inline(tmp, rndr, work.data, work.size);
+		if (rndr->cb.paragraph)
+			rndr->cb.paragraph(ob, tmp, rndr->opaque);
+		rndr_popbuf(rndr, BUFFER_BLOCK);
+	} else {
+		struct buf *header_work;
+
+		if (work.size) {
+			size_t beg;
+			i = work.size;
+			work.size -= 1;
+
+			while (work.size && data[work.size] != '\n')
+				work.size -= 1;
+
+			beg = work.size + 1;
+			while (work.size && data[work.size - 1] == '\n')
+				work.size -= 1;
+
+			if (work.size > 0) {
+				struct buf *tmp = rndr_newbuf(rndr, BUFFER_BLOCK);
+				parse_inline(tmp, rndr, work.data, work.size);
+
+				if (rndr->cb.paragraph)
+					rndr->cb.paragraph(ob, tmp, rndr->opaque);
+
+				rndr_popbuf(rndr, BUFFER_BLOCK);
+				work.data += beg;
+				work.size = i - beg;
+			}
+			else work.size = i;
+		}
+
+		header_work = rndr_newbuf(rndr, BUFFER_SPAN);
+		parse_inline(header_work, rndr, work.data, work.size);
+
+		if (rndr->cb.header)
+			rndr->cb.header(ob, header_work, (int)level, rndr->opaque);
+
+		rndr_popbuf(rndr, BUFFER_SPAN);
+	}
+
+	return end;
+}
+
+/* parse_fencedcode • handles parsing of a block-level code fragment */
+static size_t
+parse_fencedcode(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t beg, end;
+	struct buf *work = 0;
+	struct buf lang = { 0, 0, 0, 0, 0 };
+
+	beg = is_codefence(data, size, &lang);
+	if (beg == 0) return 0;
+
+	work = rndr_newbuf(rndr, BUFFER_BLOCK);
+
+	while (beg < size) {
+		size_t fence_end;
+
+		fence_end = is_codefence(data + beg, size - beg, NULL);
+		if (fence_end != 0) {
+			beg += fence_end;
+			break;
+		}
+
+		for (end = beg + 1; end < size && data[end - 1] != '\n'; end++);
+
+		if (beg < end) {
+			/* verbatim copy to the working buffer,
+				escaping entities */
+			if (is_empty(data + beg, end - beg))
+				bufputc(work, '\n');
+			else bufput(work, data + beg, end - beg);
+		}
+		beg = end;
+	}
+
+	if (work->size && work->data[work->size - 1] != '\n')
+		bufputc(work, '\n');
+
+	if (rndr->cb.blockcode)
+		rndr->cb.blockcode(ob, work, lang.size ? &lang : NULL, rndr->opaque);
+
+	rndr_popbuf(rndr, BUFFER_BLOCK);
+	return beg;
+}
+
+static size_t
+parse_blockcode(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t beg, end, pre;
+	struct buf *work = 0;
+
+	work = rndr_newbuf(rndr, BUFFER_BLOCK);
+
+	beg = 0;
+	while (beg < size) {
+		for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {};
+		pre = prefix_code(data + beg, end - beg);
+
+		if (pre)
+			beg += pre; /* skipping prefix */
+		else if (!is_empty(data + beg, end - beg))
+			/* non-empty non-prefixed line breaks the pre */
+			break;
+
+		if (beg < end) {
+			/* verbatim copy to the working buffer,
+				escaping entities */
+			if (is_empty(data + beg, end - beg))
+				bufputc(work, '\n');
+			else bufput(work, data + beg, end - beg);
+		}
+		beg = end;
+	}
+
+	while (work->size && work->data[work->size - 1] == '\n')
+		work->size -= 1;
+
+	bufputc(work, '\n');
+
+	if (rndr->cb.blockcode)
+		rndr->cb.blockcode(ob, work, NULL, rndr->opaque);
+
+	rndr_popbuf(rndr, BUFFER_BLOCK);
+	return beg;
+}
+
+/* parse_listitem • parsing of a single list item */
+/*	assuming initial prefix is already removed */
+static size_t
+parse_listitem(struct buf *ob, struct render *rndr, char *data, size_t size, int *flags)
+{
+	struct buf *work = 0, *inter = 0;
+	size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
+	int in_empty = 0, has_inside_empty = 0;
+
+	/* keeping track of the first indentation prefix */
+	while (orgpre < 3 && orgpre < size && data[orgpre] == ' ')
+		orgpre++;
+
+	beg = prefix_uli(data, size);
+	if (!beg)
+		beg = prefix_oli(data, size);
+
+	if (!beg)
+		return 0;
+
+	/* skipping to the beginning of the following line */
+	end = beg;
+	while (end < size && data[end - 1] != '\n')
+		end++;
+
+	/* getting working buffers */
+	work = rndr_newbuf(rndr, BUFFER_SPAN);
+	inter = rndr_newbuf(rndr, BUFFER_SPAN);
+
+	/* putting the first line into the working buffer */
+	bufput(work, data + beg, end - beg);
+	beg = end;
+
+	/* process the following lines */
+	while (beg < size) {
+		end++;
+
+		while (end < size && data[end - 1] != '\n')
+			end++;
+
+		/* process an empty line */
+		if (is_empty(data + beg, end - beg)) {
+			in_empty = 1;
+			beg = end;
+			continue;
+		}
+
+		/* calculating the indentation */
+		i = 0;
+		while (i < 4 && beg + i < end && data[beg + i] == ' ')
+			i++;
+
+		pre = i;
+		if (data[beg] == '\t') { i = 1; pre = 8; }
+
+		/* checking for a new item */
+		if ((prefix_uli(data + beg + i, end - beg - i) &&
+			!is_hrule(data + beg + i, end - beg - i)) ||
+			prefix_oli(data + beg + i, end - beg - i)) {
+			if (in_empty)
+				has_inside_empty = 1;
+
+			if (pre == orgpre) /* the following item must have */
+				break;             /* the same indentation */
+
+			if (!sublist)
+				sublist = work->size;
+		}
+		/* joining only indented stuff after empty lines */
+		else if (in_empty && i < 4 && data[beg] != '\t') {
+				*flags |= MKD_LI_END;
+				break;
+		}
+		else if (in_empty) {
+			bufputc(work, '\n');
+			has_inside_empty = 1;
+		}
+
+		in_empty = 0;
+
+		/* adding the line without prefix into the working buffer */
+		bufput(work, data + beg + i, end - beg - i);
+		beg = end;
+	}
+
+	/* render of li contents */
+	if (has_inside_empty)
+		*flags |= MKD_LI_BLOCK;
+
+	if (*flags & MKD_LI_BLOCK) {
+		/* intermediate render of block li */
+		if (sublist && sublist < work->size) {
+			parse_block(inter, rndr, work->data, sublist);
+			parse_block(inter, rndr, work->data + sublist, work->size - sublist); 
+		}
+		else
+			parse_block(inter, rndr, work->data, work->size);
+	} else {
+		/* intermediate render of inline li */
+		if (sublist && sublist < work->size) {
+			parse_inline(inter, rndr, work->data, sublist);
+			parse_block(inter, rndr, work->data + sublist, work->size - sublist);
+		}
+		else
+			parse_inline(inter, rndr, work->data, work->size);
+	}
+
+	/* render of li itself */
+	if (rndr->cb.listitem)
+		rndr->cb.listitem(ob, inter, *flags, rndr->opaque);
+
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	return beg;
+}
+
+
+/* parse_list • parsing ordered or unordered list block */
+static size_t
+parse_list(struct buf *ob, struct render *rndr, char *data, size_t size, int flags)
+{
+	struct buf *work = 0;
+	size_t i = 0, j;
+
+	work = rndr_newbuf(rndr, BUFFER_BLOCK);
+
+	while (i < size) {
+		j = parse_listitem(work, rndr, data + i, size - i, &flags);
+		i += j;
+
+		if (!j || (flags & MKD_LI_END))
+			break;
+	}
+
+	if (rndr->cb.list)
+		rndr->cb.list(ob, work, flags, rndr->opaque);
+	rndr_popbuf(rndr, BUFFER_BLOCK);
+	return i;
+}
+
+/* parse_atxheader • parsing of atx-style headers */
+static size_t
+parse_atxheader(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t level = 0;
+	size_t i, end, skip;
+
+	while (level < size && level < 6 && data[level] == '#')
+		level++;
+
+	for (i = level; i < size && (data[i] == ' ' || data[i] == '\t'); i++);
+
+	for (end = i; end < size && data[end] != '\n'; end++);
+	skip = end;
+
+	while (end && data[end - 1] == '#')
+		end--;
+
+	while (end && (data[end - 1] == ' ' || data[end - 1] == '\t'))
+		end--;
+
+	if (end > i) {
+		struct buf *work = rndr_newbuf(rndr, BUFFER_SPAN);
+
+		parse_inline(work, rndr, data + i, end - i);
+
+		if (rndr->cb.header)
+			rndr->cb.header(ob, work, (int)level, rndr->opaque);
+
+		rndr_popbuf(rndr, BUFFER_SPAN);
+	}
+
+	return skip;
+}
+
+
+/* htmlblock_end • checking end of HTML block : </tag>[ \t]*\n[ \t*]\n */
+/*	returns the length on match, 0 otherwise */
+static size_t
+htmlblock_end(struct html_tag *tag, struct render *rndr, char *data, size_t size)
+{
+	size_t i, w;
+
+	/* assuming data[0] == '<' && data[1] == '/' already tested */
+
+	/* checking if tag is a match */
+	if (tag->size + 3 >= size
+	|| strncasecmp(data + 2, tag->text, tag->size)
+	|| data[tag->size + 2] != '>')
+		return 0;
+
+	/* checking white lines */
+	i = tag->size + 3;
+	w = 0;
+	if (i < size && (w = is_empty(data + i, size - i)) == 0)
+		return 0; /* non-blank after tag */
+	i += w;
+	w = 0;
+
+	if (rndr->ext_flags & MKDEXT_LAX_HTML_BLOCKS) {
+		if (i < size)
+			w = is_empty(data + i, size - i);
+	} else  {
+		if (i < size && (w = is_empty(data + i, size - i)) == 0)
+			return 0; /* non-blank line after tag line */
+	}
+
+	return i + w;
+}
+
+
+/* parse_htmlblock • parsing of inline HTML block */
+static size_t
+parse_htmlblock(struct buf *ob, struct render *rndr, char *data, size_t size, int do_render)
+{
+	size_t i, j = 0;
+	struct html_tag *curtag;
+	int found;
+	struct buf work = { data, 0, 0, 0, 0 };
+
+	/* identification of the opening tag */
+	if (size < 2 || data[0] != '<') return 0;
+	curtag = find_block_tag(data + 1, size - 1);
+
+	/* handling of special cases */
+	if (!curtag) {
+
+		/* HTML comment, laxist form */
+		if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
+			i = 5;
+
+			while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>'))
+				i++;
+
+			i++;
+
+			if (i < size)
+				j = is_empty(data + i, size - i);
+
+			if (j) {
+				work.size = i + j;
+				if (do_render && rndr->cb.blockhtml)
+					rndr->cb.blockhtml(ob, &work, rndr->opaque);
+				return work.size;
+			} 
+		}
+
+		/* HR, which is the only self-closing block tag considered */
+		if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) {
+			i = 3;
+			while (i < size && data[i] != '>')
+				i++;
+
+			if (i + 1 < size) {
+				i++;
+				j = is_empty(data + i, size - i);
+				if (j) {
+					work.size = i + j;
+					if (do_render && rndr->cb.blockhtml)
+						rndr->cb.blockhtml(ob, &work, rndr->opaque);
+					return work.size;
+				}
+			} 
+		}
+
+		/* no special case recognised */
+		return 0;
+	}
+
+	/* looking for an unindented matching closing tag */
+	/*	followed by a blank line */
+	i = 1;
+	found = 0;
+
+	/* if not found, trying a second pass looking for indented match */
+	/* but not if tag is "ins" or "del" (following original Markdown.pl) */
+	if (curtag != INS_TAG && curtag != DEL_TAG) {
+		i = 1;
+		while (i < size) {
+			i++;
+			while (i < size && !(data[i - 1] == '<' && data[i] == '/'))
+				i++;
+
+			if (i + 2 + curtag->size >= size)
+				break;
+
+			j = htmlblock_end(curtag, rndr, data + i - 1, size - i + 1);
+
+			if (j) {
+				i += j - 1;
+				found = 1;
+				break;
+			}
+		} 
+	}
+
+	if (!found) return 0;
+
+	/* the end of the block has been found */
+	work.size = i;
+	if (do_render && rndr->cb.blockhtml)
+		rndr->cb.blockhtml(ob, &work, rndr->opaque);
+
+	return i;
+}
+
+static void
+parse_table_row(struct buf *ob, struct render *rndr, char *data, size_t size, size_t columns, int *col_data)
+{
+	size_t i = 0, col;
+	struct buf *row_work = 0;
+
+	if (!rndr->cb.table_cell || !rndr->cb.table_row)
+		return;
+
+	row_work = rndr_newbuf(rndr, BUFFER_SPAN);
+
+	if (i < size && data[i] == '|')
+		i++;
+
+	for (col = 0; col < columns && i < size; ++col) {
+		size_t cell_start, cell_end;
+		struct buf *cell_work;
+
+		cell_work = rndr_newbuf(rndr, BUFFER_SPAN);
+
+		while (i < size && isspace(data[i]))
+			i++;
+
+		cell_start = i;
+
+		while (i < size && data[i] != '|')
+			i++;
+
+		cell_end = i - 1;
+
+		while (cell_end > cell_start && isspace(data[cell_end]))
+			cell_end--;
+
+		parse_inline(cell_work, rndr, data + cell_start, 1 + cell_end - cell_start);
+		rndr->cb.table_cell(row_work, cell_work, col_data[col], rndr->opaque);
+
+		rndr_popbuf(rndr, BUFFER_SPAN);
+		i++;
+	}
+
+	for (; col < columns; ++col) {
+		struct buf empty_cell = {0, 0, 0, 0, 0};
+		rndr->cb.table_cell(row_work, &empty_cell, col_data[col], rndr->opaque);
+	}
+
+	rndr->cb.table_row(ob, row_work, rndr->opaque);
+
+	rndr_popbuf(rndr, BUFFER_SPAN);
+}
+
+static size_t
+parse_table_header(struct buf *ob, struct render *rndr, char *data, size_t size, size_t *columns, int **column_data)
+{
+	int pipes;
+	size_t i = 0, col, header_end, under_end;
+
+	pipes = 0;
+	while (i < size && data[i] != '\n')
+		if (data[i++] == '|')
+			pipes++;
+
+	if (i == size || pipes == 0)
+		return 0;
+
+	header_end = i;
+
+	if (data[0] == '|')
+		pipes--;
+
+	if (i > 2 && data[i - 1] == '|')
+		pipes--;
+
+	*columns = pipes + 1;
+	*column_data = calloc(*columns, sizeof(int));
+
+	/* Parse the header underline */
+	i++;
+	if (i < size && data[i] == '|')
+		i++;
+
+	under_end = i;
+	while (under_end < size && data[under_end] != '\n')
+		under_end++;
+
+	for (col = 0; col < *columns && i < under_end; ++col) {
+		size_t dashes = 0;
+
+		(*column_data)[col] |= MKD_TABLE_HEADER;
+
+		while (i < under_end && (data[i] == ' ' || data[i] == '\t'))
+			i++;
+
+		if (data[i] == ':') {
+			i++; (*column_data)[col] |= MKD_TABLE_ALIGN_L;
+			dashes++;
+		}
+
+		while (i < under_end && data[i] == '-') {
+			i++; dashes++;
+		}
+
+		if (i < under_end && data[i] == ':') {
+			i++; (*column_data)[col] |= MKD_TABLE_ALIGN_R;
+			dashes++;
+		}
+
+		while (i < under_end && (data[i] == ' ' || data[i] == '\t'))
+			i++;
+
+		if (i < under_end && data[i] != '|')
+			break;
+
+		if (dashes < 3)
+			break;
+
+		i++;
+	}
+
+	if (col < *columns)
+		return 0;
+
+	parse_table_row(ob, rndr, data, header_end, *columns, *column_data);
+	return under_end + 1;
+}
+
+static size_t
+parse_table(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t i;
+
+	struct buf *header_work = 0;
+	struct buf *body_work = 0;
+
+	size_t columns;
+	int *col_data = NULL;
+
+	header_work = rndr_newbuf(rndr, BUFFER_SPAN);
+	body_work = rndr_newbuf(rndr, BUFFER_BLOCK);
+
+	i = parse_table_header(header_work, rndr, data, size, &columns, &col_data);
+	if (i > 0) {
+
+		while (i < size) {
+			size_t row_start;
+			int pipes = 0;
+
+			row_start = i;
+
+			while (i < size && data[i] != '\n')
+				if (data[i++] == '|')
+					pipes++;
+
+			if (pipes == 0 || i == size) {
+				i = row_start;
+				break;
+			}
+
+			parse_table_row(body_work, rndr, data + row_start, i - row_start, columns, col_data);
+			i++;
+		}
+
+		if (rndr->cb.table)
+			rndr->cb.table(ob, header_work, body_work, rndr->opaque);
+	}
+
+	free(col_data);
+	rndr_popbuf(rndr, BUFFER_SPAN);
+	rndr_popbuf(rndr, BUFFER_BLOCK);
+	return i;
+}
+
+/* parse_block • parsing of one block, returning next char to parse */
+static void
+parse_block(struct buf *ob, struct render *rndr, char *data, size_t size)
+{
+	size_t beg, end, i;
+	char *txt_data;
+	beg = 0;
+
+	if (rndr->work_bufs[BUFFER_SPAN].size +
+		rndr->work_bufs[BUFFER_BLOCK].size > (int)rndr->max_nesting)
+		return;
+
+	while (beg < size) {
+		txt_data = data + beg;
+		end = size - beg;
+
+		if (is_atxheader(rndr, txt_data, end))
+			beg += parse_atxheader(ob, rndr, txt_data, end);
+
+		else if (data[beg] == '<' && rndr->cb.blockhtml &&
+				(i = parse_htmlblock(ob, rndr, txt_data, end, 1)) != 0)
+			beg += i;
+
+		else if ((i = is_empty(txt_data, end)) != 0)
+			beg += i;
+
+		else if (is_hrule(txt_data, end)) {
+			if (rndr->cb.hrule)
+				rndr->cb.hrule(ob, rndr->opaque);
+
+			while (beg < size && data[beg] != '\n')
+				beg++;
+
+			beg++;
+		}
+
+		else if ((rndr->ext_flags & MKDEXT_FENCED_CODE) != 0 &&
+			(i = parse_fencedcode(ob, rndr, txt_data, end)) != 0)
+			beg += i;
+
+		else if ((rndr->ext_flags & MKDEXT_TABLES) != 0 &&
+			(i = parse_table(ob, rndr, txt_data, end)) != 0)
+			beg += i;
+
+		else if (prefix_quote(txt_data, end))
+			beg += parse_blockquote(ob, rndr, txt_data, end);
+
+		else if (prefix_code(txt_data, end))
+			beg += parse_blockcode(ob, rndr, txt_data, end);
+
+		else if (prefix_uli(txt_data, end))
+			beg += parse_list(ob, rndr, txt_data, end, 0);
+
+		else if (prefix_oli(txt_data, end))
+			beg += parse_list(ob, rndr, txt_data, end, MKD_LIST_ORDERED);
+
+		else
+			beg += parse_paragraph(ob, rndr, txt_data, end);
+	}
+}
+
+
+
+/*********************
+ * REFERENCE PARSING *
+ *********************/
+
+/* is_ref • returns whether a line is a reference or not */
+static int
+is_ref(char *data, size_t beg, size_t end, size_t *last, struct array *refs)
+{
+/*	int n; */
+	size_t i = 0;
+	size_t id_offset, id_end;
+	size_t link_offset, link_end;
+	size_t title_offset, title_end;
+	size_t line_end;
+	struct link_ref *lr;
+/*	struct buf id = { 0, 0, 0, 0, 0 }; / * volatile buf for id search */
+
+	/* up to 3 optional leading spaces */
+	if (beg + 3 >= end) return 0;
+	if (data[beg] == ' ') { i = 1;
+	if (data[beg + 1] == ' ') { i = 2;
+	if (data[beg + 2] == ' ') { i = 3;
+	if (data[beg + 3] == ' ') return 0; } } }
+	i += beg;
+
+	/* id part: anything but a newline between brackets */
+	if (data[i] != '[') return 0;
+	i++;
+	id_offset = i;
+	while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
+		i++;
+	if (i >= end || data[i] != ']') return 0;
+	id_end = i;
+
+	/* spacer: colon (space | tab)* newline? (space | tab)* */
+	i++;
+	if (i >= end || data[i] != ':') return 0;
+	i++;
+	while (i < end && (data[i] == ' ' || data[i] == '\t')) i++;
+	if (i < end && (data[i] == '\n' || data[i] == '\r')) {
+		i++;
+		if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; }
+	while (i < end && (data[i] == ' ' || data[i] == '\t')) i++;
+	if (i >= end) return 0;
+
+	/* link: whitespace-free sequence, optionally between angle brackets */
+	if (data[i] == '<') i++;
+	link_offset = i;
+	while (i < end && data[i] != ' ' && data[i] != '\t'
+			&& data[i] != '\n' && data[i] != '\r') i++;
+	if (data[i - 1] == '>') link_end = i - 1;
+	else link_end = i;
+
+	/* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
+	while (i < end && (data[i] == ' ' || data[i] == '\t')) i++;
+	if (i < end && data[i] != '\n' && data[i] != '\r'
+			&& data[i] != '\'' && data[i] != '"' && data[i] != '(')
+		return 0;
+	line_end = 0;
+	/* computing end-of-line */
+	if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i;
+	if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
+		line_end = i + 1;
+
+	/* optional (space|tab)* spacer after a newline */
+	if (line_end) {
+		i = line_end + 1;
+		while (i < end && (data[i] == ' ' || data[i] == '\t')) i++; }
+
+	/* optional title: any non-newline sequence enclosed in '"()
+					alone on its line */
+	title_offset = title_end = 0;
+	if (i + 1 < end
+	&& (data[i] == '\'' || data[i] == '"' || data[i] == '(')) {
+		i++;
+		title_offset = i;
+		/* looking for EOL */
+		while (i < end && data[i] != '\n' && data[i] != '\r') i++;
+		if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
+			title_end = i + 1;
+		else	title_end = i;
+		/* stepping back */
+		i -= 1;
+		while (i > title_offset && (data[i] == ' ' || data[i] == '\t'))
+			i -= 1;
+		if (i > title_offset
+		&& (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
+			line_end = title_end;
+			title_end = i; } }
+	if (!line_end) return 0; /* garbage after the link */
+
+	/* a valid ref has been found, filling-in return structures */
+	if (last) *last = line_end;
+	if (!refs) return 1;
+	lr = arr_item(refs, arr_newitem(refs));
+	lr->id = bufnew(id_end - id_offset);
+	bufput(lr->id, data + id_offset, id_end - id_offset);
+	lr->link = bufnew(link_end - link_offset);
+	bufput(lr->link, data + link_offset, link_end - link_offset);
+	if (title_end > title_offset) {
+		lr->title = bufnew(title_end - title_offset);
+		bufput(lr->title, data + title_offset,
+					title_end - title_offset); }
+	else lr->title = 0;
+	return 1; 
+}
+
+static void expand_tabs(struct buf *ob, const char *line, size_t size)
+{
+	size_t  i = 0, tab = 0;
+
+	while (i < size) {
+		size_t org = i;
+
+		while (i < size && line[i] != '\t') {
+			i++; tab++;
+		}
+
+		if (i > org)
+			bufput(ob, line + org, i - org);
+
+		if (i >= size)
+			break;
+
+		do {
+			bufputc(ob, ' '); tab++;
+		} while (tab % 4);
+
+		i++;
+	}
+}
+
+/**********************
+ * EXPORTED FUNCTIONS *
+ **********************/
+
+/* markdown • parses the input buffer and renders it into the output buffer */
+void
+sd_markdown(struct buf *ob,
+	const struct buf *ib,
+	unsigned int extensions,
+	const struct sd_callbacks *callbacks,
+	void *opaque) {
+
+	static const float MARKDOWN_GROW_FACTOR = 1.4f;
+
+	struct link_ref *lr;
+	struct buf *text;
+	size_t i, beg, end;
+	struct render rndr;
+
+	/* filling the render structure */
+	if (!callbacks)
+		return;
+
+	text = bufnew(64);
+	if (!text)
+		return;
+
+	/* Preallocate enough space for our buffer to avoid expanding while copying */
+	bufgrow(text, ib->size);
+
+	memcpy(&rndr.cb, callbacks, sizeof(struct sd_callbacks));
+	arr_init(&rndr.refs, sizeof (struct link_ref));
+	parr_init(&rndr.work_bufs[BUFFER_BLOCK]);
+	parr_init(&rndr.work_bufs[BUFFER_SPAN]);
+
+/*	for (i = 0; i < 256; i++)
+		rndr.active_char[i] = 0; */
+
+	memset(rndr.active_char, 0x0, 256);
+
+	if (rndr.cb.emphasis || rndr.cb.double_emphasis || rndr.cb.triple_emphasis) {
+		rndr.active_char['*'] = MD_CHAR_EMPHASIS;
+		rndr.active_char['_'] = MD_CHAR_EMPHASIS;
+		if (extensions & MKDEXT_STRIKETHROUGH)
+			rndr.active_char['~'] = MD_CHAR_EMPHASIS;
+	}
+
+	if (rndr.cb.codespan)
+		rndr.active_char['`'] = MD_CHAR_CODESPAN;
+
+	if (rndr.cb.linebreak)
+		rndr.active_char['\n'] = MD_CHAR_LINEBREAK;
+
+	if (rndr.cb.image || rndr.cb.link)
+		rndr.active_char['['] = MD_CHAR_LINK;
+
+	rndr.active_char['<'] = MD_CHAR_LANGLE;
+	rndr.active_char['\\'] = MD_CHAR_ESCAPE;
+	rndr.active_char['&'] = MD_CHAR_ENTITITY;
+
+	if (extensions & MKDEXT_AUTOLINK) {
+		rndr.active_char[':'] = MD_CHAR_AUTOLINK_URL;
+		rndr.active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
+		rndr.active_char['w'] = MD_CHAR_AUTOLINK_WWW;
+	}
+
+	if (extensions & MKDEXT_SUPERSCRIPT)
+		rndr.active_char['^'] = MD_CHAR_SUPERSCRIPT;
+
+	/* Extension data */
+	rndr.ext_flags = extensions;
+	rndr.opaque = opaque;
+	rndr.max_nesting = 16;
+
+	/* first pass: looking for references, copying everything else */
+	beg = 0;
+	while (beg < ib->size) /* iterating over lines */
+		if (is_ref(ib->data, beg, ib->size, &end, &rndr.refs))
+			beg = end;
+		else { /* skipping to the next line */
+			end = beg;
+			while (end < ib->size && ib->data[end] != '\n' && ib->data[end] != '\r')
+				end++;
+
+			/* adding the line body if present */
+			if (end > beg)
+				expand_tabs(text, ib->data + beg, end - beg);
+
+			while (end < ib->size && (ib->data[end] == '\n' || ib->data[end] == '\r')) {
+				/* add one \n per newline */
+				if (ib->data[end] == '\n' || (end + 1 < ib->size && ib->data[end + 1] != '\n'))
+					bufputc(text, '\n');
+				end++;
+			}
+
+			beg = end;
+		}
+
+	/* sorting the reference array */
+	if (rndr.refs.size)
+		qsort(rndr.refs.base, rndr.refs.size, rndr.refs.unit, cmp_link_ref_sort);
+
+	/* pre-grow the output buffer to minimize allocations */
+	bufgrow(ob, text->size * MARKDOWN_GROW_FACTOR);
+
+	/* second pass: actual rendering */
+	if (rndr.cb.doc_header)
+		rndr.cb.doc_header(ob, rndr.opaque);
+
+	if (text->size) {
+		/* adding a final newline if not already present */
+		if (text->data[text->size - 1] != '\n' &&  text->data[text->size - 1] != '\r')
+			bufputc(text, '\n');
+
+		parse_block(ob, &rndr, text->data, text->size);
+	}
+
+	if (rndr.cb.doc_footer)
+		rndr.cb.doc_footer(ob, rndr.opaque);
+
+	/* clean-up */
+	bufrelease(text);
+	lr = rndr.refs.base;
+	for (i = 0; i < (size_t)rndr.refs.size; i++) {
+		bufrelease(lr[i].id);
+		bufrelease(lr[i].link);
+		bufrelease(lr[i].title);
+	}
+
+	arr_free(&rndr.refs);
+
+	assert(rndr.work_bufs[BUFFER_SPAN].size == 0);
+	assert(rndr.work_bufs[BUFFER_BLOCK].size == 0);
+
+	for (i = 0; i < (size_t)rndr.work_bufs[BUFFER_SPAN].asize; ++i)
+		bufrelease(rndr.work_bufs[BUFFER_SPAN].item[i]);
+
+	for (i = 0; i < (size_t)rndr.work_bufs[BUFFER_BLOCK].asize; ++i)
+		bufrelease(rndr.work_bufs[BUFFER_BLOCK].item[i]);
+
+	parr_free(&rndr.work_bufs[BUFFER_SPAN]);
+	parr_free(&rndr.work_bufs[BUFFER_BLOCK]);
+}
+
+void
+sd_version(int *ver_major, int *ver_minor, int *ver_revision)
+{
+	*ver_major = UPSKIRT_VER_MAJOR;
+	*ver_minor = UPSKIRT_VER_MINOR;
+	*ver_revision = UPSKIRT_VER_REVISION;
+}
+
+/* vim: set filetype=c: */
diff --git a/cbits/markdown.h b/cbits/markdown.h
new file mode 100644
--- /dev/null
+++ b/cbits/markdown.h
@@ -0,0 +1,120 @@
+/* markdown.h - generic markdown parser */
+
+/*
+ * Copyright (c) 2009, Natacha Porté
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UPSKIRT_MARKDOWN_H
+#define UPSKIRT_MARKDOWN_H
+
+#include "buffer.h"
+#include "autolink.h"
+
+#define UPSKIRT_VERSION "1.15.2"
+#define UPSKIRT_VER_MAJOR 1
+#define UPSKIRT_VER_MINOR 15
+#define UPSKIRT_VER_REVISION 2
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* mkd_autolink - type of autolink */
+enum mkd_autolink {
+	MKDA_NOT_AUTOLINK,	/* used internally when it is not an autolink*/
+	MKDA_NORMAL,		/* normal http/http/ftp/mailto/etc link */
+	MKDA_EMAIL,			/* e-mail link without explit mailto: */
+};
+
+enum mkd_tableflags {
+	MKD_TABLE_ALIGN_L = 1,
+	MKD_TABLE_ALIGN_R = 2,
+	MKD_TABLE_ALIGN_CENTER = 3,
+	MKD_TABLE_ALIGNMASK = 3,
+	MKD_TABLE_HEADER = 4
+};
+
+enum mkd_extensions {
+	MKDEXT_NO_INTRA_EMPHASIS = (1 << 0),
+	MKDEXT_TABLES = (1 << 1),
+	MKDEXT_FENCED_CODE = (1 << 2),
+	MKDEXT_AUTOLINK = (1 << 3),
+	MKDEXT_STRIKETHROUGH = (1 << 4),
+	MKDEXT_LAX_HTML_BLOCKS = (1 << 5),
+	MKDEXT_SPACE_HEADERS = (1 << 6),
+	MKDEXT_SUPERSCRIPT = (1 << 7),
+};
+
+/* sd_callbacks - functions for rendering parsed data */
+struct sd_callbacks {
+	/* block level callbacks - NULL skips the block */
+	void (*blockcode)(struct buf *ob, struct buf *text, struct buf *lang, void *opaque);
+	void (*blockquote)(struct buf *ob, struct buf *text, void *opaque);
+	void (*blockhtml)(struct buf *ob, struct buf *text, void *opaque);
+	void (*header)(struct buf *ob, struct buf *text, int level, void *opaque);
+	void (*hrule)(struct buf *ob, void *opaque);
+	void (*list)(struct buf *ob, struct buf *text, int flags, void *opaque);
+	void (*listitem)(struct buf *ob, struct buf *text, int flags, void *opaque);
+	void (*paragraph)(struct buf *ob, struct buf *text, void *opaque);
+	void (*table)(struct buf *ob, struct buf *header, struct buf *body, void *opaque);
+	void (*table_row)(struct buf *ob, struct buf *text, void *opaque);
+	void (*table_cell)(struct buf *ob, struct buf *text, int flags, void *opaque);
+
+
+	/* span level callbacks - NULL or return 0 prints the span verbatim */
+	int (*autolink)(struct buf *ob, struct buf *link, enum mkd_autolink type, void *opaque);
+	int (*codespan)(struct buf *ob, struct buf *text, void *opaque);
+	int (*double_emphasis)(struct buf *ob, struct buf *text, void *opaque);
+	int (*emphasis)(struct buf *ob, struct buf *text, void *opaque);
+	int (*image)(struct buf *ob, struct buf *link, struct buf *title, struct buf *alt, void *opaque);
+	int (*linebreak)(struct buf *ob, void *opaque);
+	int (*link)(struct buf *ob, struct buf *link, struct buf *title, struct buf *content, void *opaque);
+	int (*raw_html_tag)(struct buf *ob, struct buf *tag, void *opaque);
+	int (*triple_emphasis)(struct buf *ob, struct buf *text, void *opaque);
+	int (*strikethrough)(struct buf *ob, struct buf *text, void *opaque);
+	int (*superscript)(struct buf *ob, struct buf *text, void *opaque);
+
+	/* low level callbacks - NULL copies input directly into the output */
+	void (*entity)(struct buf *ob, struct buf *entity, void *opaque);
+	void (*normal_text)(struct buf *ob, struct buf *text, void *opaque);
+
+	/* header and footer */
+	void (*doc_header)(struct buf *ob, void *opaque);
+	void (*doc_footer)(struct buf *ob, void *opaque);
+};
+
+/*********
+ * FLAGS *
+ *********/
+
+/* list/listitem flags */
+#define MKD_LIST_ORDERED	1
+#define MKD_LI_BLOCK		2  /* <li> containing block data */
+
+/**********************
+ * EXPORTED FUNCTIONS *
+ **********************/
+
+/* sd_markdown * parses the input buffer and renders it into the output buffer */
+extern void
+sd_markdown(struct buf *ob, const struct buf *ib, unsigned int extensions, const struct sd_callbacks *rndr, void *opaque);
+
+/* sd_version * returns the library version as major.minor.rev */
+extern void
+sd_version(int *major, int *minor, int *revision);
+
+#endif
+
+/* vim: set filetype=c: */
diff --git a/src/Text/Sundown.hs b/src/Text/Sundown.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown.hs
@@ -0,0 +1,29 @@
+{-|
+
+Bindings to the github fork of the sundown library - previously known as upskirt:
+<https://github.com/tanoku/sundown>
+
+Example usage:
+
+> import Text.Sundown
+> import Text.Sundown.Renderers.Html
+> import qualified Data.ByteString as BS
+> import qualified Data.ByteString.UTF8 as UTF8
+> import System (getArgs)
+> import Control.Monad (liftM)
+>
+> main :: IO ()
+> main = do
+>   input <- liftM (!! 0) getArgs >>= BS.readFile
+>   putStrLn $ UTF8.toString $ renderHtml input allExtensions noHtmlModes
+
+-}
+
+module Text.Sundown
+       ( module Text.Sundown.Markdown
+       ) where
+
+import Text.Sundown.Markdown
+
+
+
diff --git a/src/Text/Sundown/Buffer/Foreign.hsc b/src/Text/Sundown/Buffer/Foreign.hsc
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Buffer/Foreign.hsc
@@ -0,0 +1,53 @@
+{-# Language ForeignFunctionInterface #-}
+
+module Text.Sundown.Buffer.Foreign
+       ( Buffer (..)
+       , c_bufnew
+       , c_bufputs
+       , c_bufgrow
+       , c_bufrelease
+       ) where
+
+import Foreign
+import Foreign.C.Types
+import Foreign.C.String
+import Data.ByteString         (ByteString)
+import qualified Data.ByteString as BS
+
+#include "buffer.h"
+
+data Buffer = Buffer { bufData  :: ByteString
+                     , bufSize  :: Int
+                     , bufASize :: Int
+                     , bufUnit  :: Int
+                     , bufRef   :: Int
+                     }
+
+instance Storable Buffer where
+  sizeOf _ = (#size struct buf)
+  alignment _ = alignment (undefined :: CInt)
+  peek ptr = do
+    d  <- (#peek struct buf, data) ptr
+    dbs <- if d == nullPtr
+           then return $ BS.pack [0]
+           else BS.packCString d
+    s  <- (#peek struct buf, size) ptr
+    as <- (#peek struct buf, asize) ptr
+    u  <- (#peek struct buf, unit) ptr
+    r  <- (#peek struct buf, ref) ptr
+    return $ Buffer dbs s as u r
+  poke _ _ = error "Buffer.poke not implemented."
+
+foreign import ccall "buffer.h bufnew"
+  c_bufnew :: CSize -> IO (Ptr Buffer)
+
+c_bufputs :: Ptr Buffer -> ByteString -> IO ()
+c_bufputs buf bs = BS.useAsCString bs $ c_bufputs' buf
+foreign import ccall "buffer.h bufputs"
+  c_bufputs' :: Ptr Buffer -> CString -> IO ()
+
+foreign import ccall "buffer.h bufgrow"
+  c_bufgrow :: Ptr Buffer -> CSize -> IO CInt
+
+foreign import ccall "buffer.h bufrelease"
+  c_bufrelease :: Ptr Buffer -> IO ()
diff --git a/src/Text/Sundown/Flag.hs b/src/Text/Sundown/Flag.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Flag.hs
@@ -0,0 +1,18 @@
+module Text.Sundown.Flag
+       ( Flag (..)
+       , toCUInt
+       ) where
+
+
+import Foreign.C.Types
+import Data.Bits
+
+class Flag a where
+  flagIndexes :: a -> [(Int, Bool)]
+
+toCUInt :: Flag a => a -> CUInt
+toCUInt flag = foldl (\uint (ix, b) -> shiftL (fromIntegral . fromEnum $ b) ix .|. uint)
+                     0 (flagIndexes flag)
+
+
+
diff --git a/src/Text/Sundown/Markdown.hs b/src/Text/Sundown/Markdown.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Markdown.hs
@@ -0,0 +1,15 @@
+module Text.Sundown.Markdown
+       ( Extensions (..)
+       , allExtensions
+       , noExtensions
+       ) where
+
+import Text.Sundown.Markdown.Foreign
+
+-- | All 'Extensions' disabled
+noExtensions :: Extensions
+noExtensions = Extensions False False False False False False 
+
+-- | All 'Extensions' enabled
+allExtensions :: Extensions
+allExtensions = Extensions True True True True True True
diff --git a/src/Text/Sundown/Markdown/Foreign.hsc b/src/Text/Sundown/Markdown/Foreign.hsc
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Markdown/Foreign.hsc
@@ -0,0 +1,48 @@
+{-# Language ForeignFunctionInterface #-}
+
+module Text.Sundown.Markdown.Foreign
+       ( Callbacks
+       , Extensions (..)
+       , c_sd_markdown
+       ) where
+
+import Foreign
+import Foreign.C.Types
+
+import Text.Sundown.Buffer.Foreign
+import Text.Sundown.Flag
+
+#include "markdown.h"
+
+data Callbacks
+
+-- | A set of switches to enable or disable markdown features.
+data Extensions = Extensions { extNoIntraEmphasis :: Bool
+                             , extTables          :: Bool
+                             , extFencedCode      :: Bool
+                             , extAutolink        :: Bool
+                             , extStrikethrough   :: Bool
+                             , extLaxHtmlBlocks   :: Bool
+                             }
+
+instance Flag Extensions where
+  flagIndexes exts = [ (0, extNoIntraEmphasis exts)
+                     , (1, extTables exts)
+                     , (2, extFencedCode exts)
+                     , (3, extAutolink exts)
+                     , (4, extStrikethrough exts)
+                     , (5, extLaxHtmlBlocks exts)
+                     ]
+
+
+
+instance Storable Callbacks where
+    sizeOf _ = (#size struct sd_callbacks)
+    alignment _ = alignment (undefined :: Ptr ())
+    peek _ = error "Callbacks.peek is not implemented"
+    poke _ _ = error "Callbacks.poke is not implemented"
+
+c_sd_markdown :: Ptr Buffer -> Ptr Buffer -> Extensions -> Ptr Callbacks -> Ptr () -> IO ()
+c_sd_markdown ob ib exts rndr opaque = c_sd_markdown' ob ib (toCUInt exts) rndr opaque
+foreign import ccall "markdown.h sd_markdown"
+  c_sd_markdown' :: Ptr Buffer -> Ptr Buffer -> CUInt -> Ptr Callbacks -> Ptr () -> IO ()
diff --git a/src/Text/Sundown/Renderers/Html.hs b/src/Text/Sundown/Renderers/Html.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Renderers/Html.hs
@@ -0,0 +1,72 @@
+{-# Language ForeignFunctionInterface #-}
+
+module Text.Sundown.Renderers.Html
+       ( renderHtml
+       , noHtmlModes
+       , allHtmlModes
+       , smartypants
+       , HtmlRenderMode (..)
+       ) where
+
+
+import Foreign
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+
+import Text.Sundown.Markdown.Foreign
+import Text.Sundown.Buffer.Foreign
+import Text.Sundown.Renderers.Html.Foreign
+
+-- | Parses a 'ByteString' containing the markdown, returns the Html
+-- code.
+renderHtml :: ByteString -> Extensions -> HtmlRenderMode -> ByteString
+renderHtml input exts mode =
+  unsafePerformIO $
+  alloca $ \renderer ->
+  alloca $ \options -> do
+    -- Allocate buffers
+    ob <- c_bufnew 64
+    ib <- c_bufnew . fromIntegral . BS.length $ input
+    
+    -- Put the input content into the buffer
+    c_bufputs ib input
+    
+    -- Do the markdown
+    c_sdhtml_renderer renderer options mode
+    c_sd_markdown ob ib exts renderer (castPtr options)
+    
+    -- Get the result
+    Buffer {bufData = output} <- peek ob
+    
+    c_bufrelease ib
+    c_bufrelease ob    
+    
+    return output
+
+-- | All the 'HtmlRenderMode' disabled
+noHtmlModes :: HtmlRenderMode
+noHtmlModes = HtmlRenderMode False False False False False False False False False False
+
+-- | All the 'HtmlRenderMode' enabled
+allHtmlModes :: HtmlRenderMode
+allHtmlModes = HtmlRenderMode True True True True True True True True True True
+
+-- | Converts punctuation in Html entities,
+-- <http://daringfireball.net/projects/smartypants/>
+smartypants :: ByteString -> ByteString
+smartypants input =
+  unsafePerformIO $ do
+    ob <- c_bufnew 64
+    ib <- c_bufnew $ fromInteger . toInteger $ BS.length input
+    
+    c_bufputs ib input
+    
+    c_sdhtml_smartypants ob ib
+    
+    Buffer {bufData = output} <- peek ob
+    
+    c_bufrelease ib
+    c_bufrelease ob
+    
+    return output
diff --git a/src/Text/Sundown/Renderers/Html/Foreign.hsc b/src/Text/Sundown/Renderers/Html/Foreign.hsc
new file mode 100644
--- /dev/null
+++ b/src/Text/Sundown/Renderers/Html/Foreign.hsc
@@ -0,0 +1,63 @@
+{-# Language ForeignFunctionInterface #-}
+
+module Text.Sundown.Renderers.Html.Foreign
+       ( HtmlRenderMode (..)
+       , c_sdhtml_renderer
+       , c_sdhtml_toc_renderer
+       , c_sdhtml_smartypants
+       ) where
+
+
+import Foreign
+import Foreign.C.Types
+
+import Text.Sundown.Buffer.Foreign
+import Text.Sundown.Markdown.Foreign
+import Text.Sundown.Flag
+
+#include "html.h"
+
+data HtmlRenderMode = HtmlRenderMode { htmlSkipHtml :: Bool
+                                     , htmlSkipStyle :: Bool
+                                     , htmlSkipImages :: Bool
+                                     , htmlSkipLinks :: Bool
+                                     , htmlExpandTabs :: Bool
+                                     , htmlSafelink :: Bool
+                                     , htmlToc :: Bool
+                                     , htmlHardWrap :: Bool
+                                     , htmlGithubBlockcode :: Bool
+                                     , htmlUseXhtml :: Bool
+                                     }
+
+
+instance Flag HtmlRenderMode where
+  flagIndexes mode = [ (0,  htmlSkipHtml mode)
+                     , (1,  htmlSkipStyle mode)
+                     , (2,  htmlSkipImages mode)
+                     , (3,  htmlSkipLinks mode)
+                     , (5,  htmlExpandTabs mode)
+                     , (7,  htmlSafelink mode)
+                     , (8,  htmlToc mode)
+                     , (9,  htmlHardWrap mode)
+                     , (10, htmlGithubBlockcode mode)
+                     , (11, htmlUseXhtml mode)
+                     ]
+
+data HtmlRenderOptions
+
+instance Storable HtmlRenderOptions where
+  sizeOf _ = (#size struct html_renderopt)
+  alignment _ = alignment (undefined :: Ptr ())
+  peek _ = error "HtmlRenderopt.peek is not implemented"
+  poke _ = error "HtmlRenderopt.poke is not implemented"
+
+c_sdhtml_renderer :: Ptr Callbacks -> Ptr HtmlRenderOptions -> HtmlRenderMode -> IO ()
+c_sdhtml_renderer rndr options mode = c_sdhtml_renderer' rndr options (toCUInt mode)
+foreign import ccall "html.h sdhtml_renderer"
+  c_sdhtml_renderer' :: Ptr Callbacks -> Ptr HtmlRenderOptions -> CUInt -> IO ()
+
+foreign import ccall "html.h sdhtml_toc_renderer"
+  c_sdhtml_toc_renderer :: Ptr Callbacks -> Ptr HtmlRenderOptions -> IO ()
+
+foreign import ccall "html.h sdhtml_smartypants"
+  c_sdhtml_smartypants :: Ptr Buffer -> Ptr Buffer -> IO ()
diff --git a/sundown.cabal b/sundown.cabal
new file mode 100644
--- /dev/null
+++ b/sundown.cabal
@@ -0,0 +1,47 @@
+Cabal-version:          >= 1.6
+Name:                   sundown
+Version:                0.1
+Author:                 Francesco Mazzoli (f@mazzo.li)
+Maintainer:             Francesco Mazzoli (f@mazzo.li)
+Build-Type:             Simple
+License:                PublicDomain
+Build-Type:             Simple
+Category:               Foreign binding, Text
+Synopsis:               Binding to upskirt
+Tested-With:            GHC==7.0.2
+Description:
+  Bindings to github's sundown, a nice C markdown library:
+  <https://github.com/tanoku/sundown>.
+
+source-repository head
+  type:     git
+  location: git://github.com/rostayob/sundown.git
+
+Library
+  Hs-Source-Dirs:       src
+
+  Build-Depends:        base >= 3 && < 5
+                      , bytestring
+
+  GHC-Options:          -Wall -O2
+  
+  Extensions:           ForeignFunctionInterface, EmptyDataDecls
+  
+  Exposed-Modules:      Text.Sundown
+                      , Text.Sundown.Markdown
+                      , Text.Sundown.Renderers.Html
+
+  Other-Modules:        Text.Sundown.Buffer.Foreign
+                      , Text.Sundown.Renderers.Html.Foreign
+                      , Text.Sundown.Flag
+                      , Text.Sundown.Markdown.Foreign
+  
+  C-Sources:            cbits/array.c, cbits/buffer.c, cbits/markdown.c,
+                        cbits/html.c, cbits/html_smartypants.c,
+                        cbits/autolink.c
+  
+  Include-Dirs:         cbits
+  
+  Includes:             array.h, autolink.h, buffer.h, markdown.h, html.h
+  
+  Install-includes:     array.h, autolink.h, buffer.h, markdown.h, html.h
