summary refs log tree commit diff
path: root/src/rt/sundown/html
diff options
context:
space:
mode:
authorAlex Crichton <alex@alexcrichton.com>2013-09-23 15:35:09 -0700
committerAlex Crichton <alex@alexcrichton.com>2013-09-25 14:27:41 -0700
commit6aba140fa738acea9d06e29e95abbfcc9d1cba5d (patch)
treed5b6dd07428938dd8851002e9e374bae537895be /src/rt/sundown/html
parentaf25f58ac3da45899ed65b3af965150c8a90dcda (diff)
rustdoc: Add sundown to src/rt/
This also starts compiling it in the same manner as linenoise, it's just bundled
with librustrt directly, and we export just a few symbols out of it.
Diffstat (limited to 'src/rt/sundown/html')
-rw-r--r--src/rt/sundown/html/houdini.h37
-rw-r--r--src/rt/sundown/html/houdini_href_e.c108
-rw-r--r--src/rt/sundown/html/houdini_html_e.c84
-rwxr-xr-xsrc/rt/sundown/html/html.c635
-rw-r--r--src/rt/sundown/html/html.h77
-rw-r--r--src/rt/sundown/html/html_smartypants.c389
6 files changed, 1330 insertions, 0 deletions
diff --git a/src/rt/sundown/html/houdini.h b/src/rt/sundown/html/houdini.h
new file mode 100644
index 00000000000..b4954c02f67
--- /dev/null
+++ b/src/rt/sundown/html/houdini.h
@@ -0,0 +1,37 @@
+#ifndef HOUDINI_H__
+#define HOUDINI_H__
+
+#include "buffer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef HOUDINI_USE_LOCALE
+#	define _isxdigit(c) isxdigit(c)
+#	define _isdigit(c) isdigit(c)
+#else
+/*
+ * Helper _isdigit methods -- do not trust the current locale
+ * */
+#	define _isxdigit(c) (strchr("0123456789ABCDEFabcdef", (c)) != NULL)
+#	define _isdigit(c) ((c) >= '0' && (c) <= '9')
+#endif
+
+extern void houdini_escape_html(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_html0(struct buf *ob, const uint8_t *src, size_t size, int secure);
+extern void houdini_unescape_html(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_xml(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_uri(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_url(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_href(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_unescape_uri(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_unescape_url(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_escape_js(struct buf *ob, const uint8_t *src, size_t size);
+extern void houdini_unescape_js(struct buf *ob, const uint8_t *src, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/rt/sundown/html/houdini_href_e.c b/src/rt/sundown/html/houdini_href_e.c
new file mode 100644
index 00000000000..981b3b17e41
--- /dev/null
+++ b/src/rt/sundown/html/houdini_href_e.c
@@ -0,0 +1,108 @@
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "houdini.h"
+
+#define ESCAPE_GROW_FACTOR(x) (((x) * 12) / 10)
+
+/*
+ * The following characters will not be escaped:
+ *
+ *		-_.+!*'(),%#@?=;:/,+&$ alphanum
+ *
+ * Note that this character set is the addition of:
+ *
+ *	- The characters which are safe to be in an URL
+ *	- The characters which are *not* safe to be in
+ *	an URL because they are RESERVED characters.
+ *
+ * We asume (lazily) that any RESERVED char that
+ * appears inside an URL is actually meant to
+ * have its native function (i.e. as an URL 
+ * component/separator) and hence needs no escaping.
+ *
+ * There are two exceptions: the chacters & (amp)
+ * and ' (single quote) do not appear in the table.
+ * They are meant to appear in the URL as components,
+ * yet they require special HTML-entity escaping
+ * to generate valid HTML markup.
+ *
+ * All other characters will be escaped to %XX.
+ *
+ */
+static const char HREF_SAFE[] = {
+	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, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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,
+};
+
+void
+houdini_escape_href(struct buf *ob, const uint8_t *src, size_t size)
+{
+	static const char hex_chars[] = "0123456789ABCDEF";
+	size_t  i = 0, org;
+	char hex_str[3];
+
+	bufgrow(ob, ESCAPE_GROW_FACTOR(size));
+	hex_str[0] = '%';
+
+	while (i < size) {
+		org = i;
+		while (i < size && HREF_SAFE[src[i]] != 0)
+			i++;
+
+		if (i > org)
+			bufput(ob, src + org, i - org);
+
+		/* escaping */
+		if (i >= size)
+			break;
+
+		switch (src[i]) {
+		/* amp appears all the time in URLs, but needs
+		 * HTML-entity escaping to be inside an href */
+		case '&': 
+			BUFPUTSL(ob, "&amp;");
+			break;
+
+		/* the single quote is a valid URL character
+		 * according to the standard; it needs HTML
+		 * entity escaping too */
+		case '\'':
+			BUFPUTSL(ob, "&#x27;");
+			break;
+		
+		/* the space can be escaped to %20 or a plus
+		 * sign. we're going with the generic escape
+		 * for now. the plus thing is more commonly seen
+		 * when building GET strings */
+#if 0
+		case ' ':
+			bufputc(ob, '+');
+			break;
+#endif
+
+		/* every other character goes with a %XX escaping */
+		default:
+			hex_str[1] = hex_chars[(src[i] >> 4) & 0xF];
+			hex_str[2] = hex_chars[src[i] & 0xF];
+			bufput(ob, hex_str, 3);
+		}
+
+		i++;
+	}
+}
diff --git a/src/rt/sundown/html/houdini_html_e.c b/src/rt/sundown/html/houdini_html_e.c
new file mode 100644
index 00000000000..d9bbf187d82
--- /dev/null
+++ b/src/rt/sundown/html/houdini_html_e.c
@@ -0,0 +1,84 @@
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "houdini.h"
+
+#define ESCAPE_GROW_FACTOR(x) (((x) * 12) / 10) /* this is very scientific, yes */
+
+/**
+ * According to the OWASP rules:
+ *
+ * & --> &amp;
+ * < --> &lt;
+ * > --> &gt;
+ * " --> &quot;
+ * ' --> &#x27;     &apos; is not recommended
+ * / --> &#x2F;     forward slash is included as it helps end an HTML entity
+ *
+ */
+static const char HTML_ESCAPE_TABLE[] = {
+	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, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4, 
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 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, 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 const char *HTML_ESCAPES[] = {
+        "",
+        "&quot;",
+        "&amp;",
+        "&#39;",
+        "&#47;",
+        "&lt;",
+        "&gt;"
+};
+
+void
+houdini_escape_html0(struct buf *ob, const uint8_t *src, size_t size, int secure)
+{
+	size_t i = 0, org, esc = 0;
+
+	bufgrow(ob, ESCAPE_GROW_FACTOR(size));
+
+	while (i < size) {
+		org = i;
+		while (i < size && (esc = HTML_ESCAPE_TABLE[src[i]]) == 0)
+			i++;
+
+		if (i > org)
+			bufput(ob, src + org, i - org);
+
+		/* escaping */
+		if (i >= size)
+			break;
+
+		/* The forward slash is only escaped in secure mode */
+		if (src[i] == '/' && !secure) {
+			bufputc(ob, '/');
+		} else {
+			bufputs(ob, HTML_ESCAPES[esc]);
+		}
+
+		i++;
+	}
+}
+
+void
+houdini_escape_html(struct buf *ob, const uint8_t *src, size_t size)
+{
+	houdini_escape_html0(ob, src, size, 1);
+}
+
diff --git a/src/rt/sundown/html/html.c b/src/rt/sundown/html/html.c
new file mode 100755
index 00000000000..7f08ee8ef1b
--- /dev/null
+++ b/src/rt/sundown/html/html.c
@@ -0,0 +1,635 @@
+/*
+ * 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>
+
+#include "houdini.h"
+
+#define USE_XHTML(opt) (opt->flags & HTML_USE_XHTML)
+
+int
+sdhtml_is_tag(const uint8_t *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;
+}
+
+static inline void escape_html(struct buf *ob, const uint8_t *source, size_t length)
+{
+	houdini_escape_html0(ob, source, length, 0);
+}
+
+static inline void escape_href(struct buf *ob, const uint8_t *source, size_t length)
+{
+	houdini_escape_href(ob, source, length);
+}
+
+/********************
+ * GENERIC RENDERER *
+ ********************/
+static int
+rndr_autolink(struct buf *ob, const 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:");
+	escape_href(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) {
+		escape_html(ob, link->data + 7, link->size - 7);
+	} else {
+		escape_html(ob, link->data, link->size);
+	}
+
+	BUFPUTSL(ob, "</a>");
+
+	return 1;
+}
+
+static void
+rndr_blockcode(struct buf *ob, const struct buf *text, const 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, ' ');
+				escape_html(ob, lang->data + org, i - org);
+			}
+		}
+
+		BUFPUTSL(ob, "\">");
+	} else
+		BUFPUTSL(ob, "<pre><code>");
+
+	if (text)
+		escape_html(ob, text->data, text->size);
+
+	BUFPUTSL(ob, "</code></pre>\n");
+}
+
+static void
+rndr_blockquote(struct buf *ob, const 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, const struct buf *text, void *opaque)
+{
+	BUFPUTSL(ob, "<code>");
+	if (text) escape_html(ob, text->data, text->size);
+	BUFPUTSL(ob, "</code>");
+	return 1;
+}
+
+static int
+rndr_strikethrough(struct buf *ob, const 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, const 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, const 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, const 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, const struct buf *link, const struct buf *title, const 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)
+		escape_href(ob, link->data, link->size);
+
+	if (title && title->size) {
+		BUFPUTSL(ob, "\" title=\"");
+		escape_html(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, const 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, const struct buf *text, int flags, void *opaque)
+{
+	BUFPUTSL(ob, "<li>");
+	if (text) {
+		size_t size = text->size;
+		while (size && text->data[size - 1] == '\n')
+			size--;
+
+		bufput(ob, text->data, size);
+	}
+	BUFPUTSL(ob, "</li>\n");
+}
+
+static void
+rndr_paragraph(struct buf *ob, const 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, const struct buf *text, void *opaque)
+{
+	size_t org, sz;
+	if (!text) return;
+	sz = text->size;
+	while (sz > 0 && text->data[sz - 1] == '\n') sz--;
+	org = 0;
+	while (org < sz && text->data[org] == '\n') org++;
+	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, const 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, const struct buf *link, const struct buf *title, const struct buf *alt, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+	if (!link || !link->size) return 0;
+
+	BUFPUTSL(ob, "<img src=\"");
+	escape_href(ob, link->data, link->size);
+	BUFPUTSL(ob, "\" alt=\"");
+
+	if (alt && alt->size)
+		escape_html(ob, alt->data, alt->size);
+
+	if (title && title->size) {
+		BUFPUTSL(ob, "\" title=\"");
+		escape_html(ob, title->data, title->size); }
+
+	bufputs(ob, USE_XHTML(options) ? "\"/>" : "\">");
+	return 1;
+}
+
+static int
+rndr_raw_html(struct buf *ob, const struct buf *text, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	/* HTML_ESCAPE overrides SKIP_HTML, SKIP_STYLE, SKIP_LINKS and SKIP_IMAGES
+	* It doens't see if there are any valid tags, just escape all of them. */
+	if((options->flags & HTML_ESCAPE) != 0) {
+		escape_html(ob, text->data, text->size);
+		return 1;
+	}
+
+	if ((options->flags & HTML_SKIP_HTML) != 0)
+		return 1;
+
+	if ((options->flags & HTML_SKIP_STYLE) != 0 &&
+		sdhtml_is_tag(text->data, text->size, "style"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_LINKS) != 0 &&
+		sdhtml_is_tag(text->data, text->size, "a"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_IMAGES) != 0 &&
+		sdhtml_is_tag(text->data, text->size, "img"))
+		return 1;
+
+	bufput(ob, text->data, text->size);
+	return 1;
+}
+
+static void
+rndr_table(struct buf *ob, const struct buf *header, const 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, const 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, const 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, const 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, const struct buf *text, void *opaque)
+{
+	if (text)
+		escape_html(ob, text->data, text->size);
+}
+
+static void
+toc_header(struct buf *ob, const struct buf *text, int level, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	/* set the level offset if this is the first header
+	 * we're parsing for the document */
+	if (options->toc_data.current_level == 0) {
+		options->toc_data.level_offset = level - 1;
+	}
+	level -= options->toc_data.level_offset;
+
+	if (level > options->toc_data.current_level) {
+		while (level > options->toc_data.current_level) {
+			BUFPUTSL(ob, "<ul>\n<li>\n");
+			options->toc_data.current_level++;
+		}
+	} else if (level < options->toc_data.current_level) {
+		BUFPUTSL(ob, "</li>\n");
+		while (level < options->toc_data.current_level) {
+			BUFPUTSL(ob, "</ul>\n</li>\n");
+			options->toc_data.current_level--;
+		}
+		BUFPUTSL(ob,"<li>\n");
+	} else {
+		BUFPUTSL(ob,"</li>\n<li>\n");
+	}
+
+	bufprintf(ob, "<a href=\"#toc_%d\">", options->toc_data.header_count++);
+	if (text)
+		escape_html(ob, text->data, text->size);
+	BUFPUTSL(ob, "</a>\n");
+}
+
+static int
+toc_link(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *content, void *opaque)
+{
+	if (content && content->size)
+		bufput(ob, content->data, content->size);
+	return 1;
+}
+
+static void
+toc_finalize(struct buf *ob, void *opaque)
+{
+	struct html_renderopt *options = opaque;
+
+	while (options->toc_data.current_level > 0) {
+		BUFPUTSL(ob, "</li>\n</ul>\n");
+		options->toc_data.current_level--;
+	}
+}
+
+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,
+		toc_link,
+		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 || render_flags & HTML_ESCAPE)
+		callbacks->blockhtml = NULL;
+}
diff --git a/src/rt/sundown/html/html.h b/src/rt/sundown/html/html.h
new file mode 100644
index 00000000000..4c8810d471c
--- /dev/null
+++ b/src/rt/sundown/html/html.h
@@ -0,0 +1,77 @@
+/*
+ * 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>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct html_renderopt {
+	struct {
+		int header_count;
+		int current_level;
+		int level_offset;
+	} toc_data;
+
+	unsigned int flags;
+
+	/* extra callbacks */
+	void (*link_attributes)(struct buf *ob, const 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 << 4),
+	HTML_SAFELINK = (1 << 5),
+	HTML_TOC = (1 << 6),
+	HTML_HARD_WRAP = (1 << 7),
+	HTML_USE_XHTML = (1 << 8),
+	HTML_ESCAPE = (1 << 9),
+} html_render_mode;
+
+typedef enum {
+	HTML_TAG_NONE = 0,
+	HTML_TAG_OPEN,
+	HTML_TAG_CLOSE,
+} html_tag;
+
+int
+sdhtml_is_tag(const uint8_t *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, const uint8_t *text, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
diff --git a/src/rt/sundown/html/html_smartypants.c b/src/rt/sundown/html/html_smartypants.c
new file mode 100644
index 00000000000..367c26aeb9c
--- /dev/null
+++ b/src/rt/sundown/html/html_smartypants.c
@@ -0,0 +1,389 @@
+/*
+ * 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>
+
+#if defined(_WIN32)
+#define snprintf	_snprintf		
+#endif
+
+struct smartypants_data {
+	int in_squote;
+	int in_dquote;
+};
+
+static size_t smartypants_cb__ltag(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__dquote(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__amp(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__period(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__number(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__dash(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__parens(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__squote(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__backtick(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__escape(struct buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+
+static size_t (*smartypants_cb_ptrs[])
+	(struct buf *, struct smartypants_data *, uint8_t, const uint8_t *, 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 uint8_t 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(uint8_t c)
+{
+	return c == 0 || isspace(c) || ispunct(c);
+}
+
+static int
+smartypants_quotes(struct buf *ob, uint8_t previous_char, uint8_t next_char, uint8_t 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, uint8_t previous_char, const uint8_t *text, size_t size)
+{
+	if (size >= 2) {
+		uint8_t 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) {
+			uint8_t 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, uint8_t previous_char, const uint8_t *text, size_t size)
+{
+	if (size >= 3) {
+		uint8_t t1 = tolower(text[1]);
+		uint8_t 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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *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, uint8_t previous_char, const uint8_t *text, size_t size)
+{
+	static const char *skip_tags[] = {
+	  "pre", "code", "var", "samp", "kbd", "math", "script", "style"
+	};
+	static const size_t skip_tags_count = 8;
+
+	size_t tag, i = 0;
+
+	while (i < size && text[i] != '>')
+		i++;
+
+	for (tag = 0; tag < skip_tags_count; ++tag) {
+		if (sdhtml_is_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_is_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, uint8_t previous_char, const uint8_t *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 {
+    uint8_t c0;
+    const uint8_t *pattern;
+    const uint8_t *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, const uint8_t *text, size_t size)
+{
+	size_t i;
+	struct smartypants_data smrt = {0, 0};
+
+	if (!text)
+		return;
+
+	bufgrow(ob, size);
+
+	for (i = 0; i < size; ++i) {
+		size_t org;
+		uint8_t action = 0;
+
+		org = i;
+		while (i < size && (action = smartypants_cb_chars[text[i]]) == 0)
+			i++;
+
+		if (i > org)
+			bufput(ob, text + org, i - org);
+
+		if (i < size) {
+			i += smartypants_cb_ptrs[(int)action]
+				(ob, &smrt, i ? text[i - 1] : 0, text + i, size - i);
+		}
+	}
+}
+
+