[lua-torch-sundown] 01/01: Imported Upstream version 0~20160713-g8353f5a

Zhou Mo cdluminate-guest at moszumanska.debian.org
Fri Aug 12 14:47:14 UTC 2016


This is an automated email from the git hooks/post-receive script.

cdluminate-guest pushed a commit to branch master
in repository lua-torch-sundown.

commit a3aaeda2c90b043e252833af248138399f61752a
Author: Zhou Mo <cdluminate at gmail.com>
Date:   Fri Aug 12 14:40:44 2016 +0000

    Imported Upstream version 0~20160713-g8353f5a
---
 .gitignore                   |   12 +
 .travis.yml                  |   40 +
 CMakeLists.txt               |   39 +
 LICENSE.md                   |   51 +
 README.md                    |  158 +++
 ascii.lua                    |  601 ++++++++++
 env.lua                      |   20 +
 html.lua                     |   23 +
 html/houdini.h               |   37 +
 html/houdini_href_e.c        |  108 ++
 html/houdini_html_e.c        |   84 ++
 html/html.c                  |  635 +++++++++++
 html/html.h                  |   77 ++
 html/html_smartypants.c      |  389 +++++++
 htmlcdefs.lua                |   45 +
 init.lua                     |    9 +
 mdcat.lua                    |    5 +
 rocks/sundown-scm-1.rockspec |   51 +
 sdcdefs.lua                  |   96 ++
 src/autolink.c               |  297 +++++
 src/autolink.h               |   51 +
 src/buffer.c                 |  225 ++++
 src/buffer.h                 |   96 ++
 src/html_blocks.h            |  206 ++++
 src/markdown.c               | 2561 ++++++++++++++++++++++++++++++++++++++++++
 src/markdown.h               |  138 +++
 src/stack.c                  |   81 ++
 src/stack.h                  |   29 +
 28 files changed, 6164 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1b68ba0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# CMake
+CMakeCache.txt
+CMakeFiles
+Makefile
+cmake_install.cmake
+install_manifest.txt
+
+# Object files
+*.o
+
+# Build output
+libsundown.so
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..799b490
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,40 @@
+language: c
+compiler:
+  - gcc
+  - clang
+sudo: false
+env:
+  - TORCH_LUA_VERSION=LUAJIT21
+  - TORCH_LUA_VERSION=LUA51
+  - TORCH_LUA_VERSION=LUA52
+addons:
+  apt:
+    packages:
+    - cmake
+    - gcc-multilib
+    - build-essential
+    - gcc
+    - g++
+    - curl
+    - cmake
+    - libreadline-dev
+    - git-core
+    - ncurses-dev
+    - unzip
+before_script:
+- export ROOT_TRAVIS_DIR=$(pwd)
+- export INSTALL_PREFIX=~/torch/install
+- git clone https://github.com/torch/distro.git ~/torch --recursive
+- cd ~/torch && git submodule update --init --recursive
+- mkdir build && cd build
+- cmake .. -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" -DCMAKE_BUILD_TYPE=Release -DWITH_${TORCH_LUA_VERSION}=ON
+- make && make install
+- cd $ROOT_TRAVIS_DIR
+- export LD_LIBRARY_PATH=${INSTALL_PREFIX}/lib:$LD_LIBRARY_PATH
+script:
+- ${INSTALL_PREFIX}/bin/luarocks install luaffi
+- ${INSTALL_PREFIX}/bin/luarocks install luabitop || true
+- ${INSTALL_PREFIX}/bin/luarocks make rocks/sundown-scm-1.rockspec
+- export PATH=${INSTALL_PREFIX}/bin:$PATH
+- export TESTLUA=$(which luajit lua | head -n 1)
+- ${TESTLUA} -lsundown -e "print('passed')"
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..f8d3133
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,39 @@
+CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR)
+CMAKE_POLICY(VERSION 2.6)
+
+INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/src)
+SET(src 
+	${CMAKE_CURRENT_SOURCE_DIR}/src/autolink.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/src/buffer.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/src/markdown.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/src/stack.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/html/houdini_href_e.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/html/houdini_html_e.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/html/html.c 
+	${CMAKE_CURRENT_SOURCE_DIR}/html/html_smartypants.c)
+SET(luasrc 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/env.lua 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/init.lua 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/sdcdefs.lua 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/htmlcdefs.lua 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/html.lua 
+	   ${CMAKE_CURRENT_SOURCE_DIR}/ascii.lua)
+
+ADD_LIBRARY("sundown" MODULE ${src})
+SET_TARGET_PROPERTIES("sundown" PROPERTIES
+  PREFIX "lib"
+  IMPORT_PREFIX "lib")
+
+IF(APPLE)
+  SET_TARGET_PROPERTIES("sundown" PROPERTIES
+    LINK_FLAGS "-undefined dynamic_lookup")
+ENDIF()
+
+INSTALL(FILES ${luasrc} DESTINATION ${LUADIR}/sundown)
+INSTALL(TARGETS sundown
+  LIBRARY DESTINATION ${LIBDIR}
+  RUNTIME DESTINATION ${LIBDIR})
+
+IF(LUALIB)
+  TARGET_LINK_LIBRARIES(sundown ${LUALIB})
+ENDIF()
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..9a1fe1d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,51 @@
+===============================================================================
+
+sundown-ffi -- http://github.com/andresy/sundown
+
+Copyright (c) 2013 Idiap Research Institute (Ronan Collobert)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. The name of Idiap Research Institute nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+===============================================================================
+
+Sundown library -- http://github.com/vmg/sundown
+
+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.
+
+===============================================================================
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d98afe2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,158 @@
+[![Build Status](https://travis-ci.org/torch/sundown-ffi.svg)](https://travis-ci.org/torch/sundown-ffi)
+
+sundown-ffi
+===========
+
+A LuaJIT interface to the Sundown library (a Markdown implementation)
+
+# Installation #
+
+torch-rocks install https://raw.github.com/andresy/sundown-ffi/master/rocks/sundown-scm-1.rockspec
+
+# Usage #
+
+## HTML
+
+To render into HTML, the easiest is to use the provided `renderHTML()` function (aliased to `render()`), which interfaces Sundown renderer with Houdini HTML default renderer.
+
+```lua
+local sundown = require 'sundown'
+
+local html = sundow.render[[
+sundown-ffi
+===========
+
+A LuaJIT interface to the Sundown library (a Markdown implementation)
+
+# Installation #
+
+torch-rocks install https://raw.github.com/andresy/sundown-ffi/master/rocks/sundown-scm-1.rockspec
+]]
+```
+
+You can equivalently call `render()` in `sundown.html`:
+```lua
+local html = require 'sundown.html'
+html.render[[
+...
+]]
+```
+
+## ASCII Markdown Pretty Print
+
+We also provide an extra renderer `renderASCII()` which outputs pretty colored ASCII for Markdown pages.
+
+```lua
+local sundown = require 'sundown'
+local text = sundown.renderASCII[[
+sundown-ffi
+===========
+
+A LuaJIT interface to the Sundown library (a Markdown implementation)
+
+# Installation #
+
+torch-rocks install https://raw.github.com/andresy/sundown-ffi/master/rocks/sundown-scm-1.rockspec
+]]
+```
+
+You can equivalently call `render()` in `sundown.ascii`:
+```lua
+local ascii = require 'sundown.ascii'
+ascii.render[[
+...
+]]
+```
+
+
+### Styles and Colors
+
+`renderASCII(text[, style])` takes an optional `style` argument, which
+defines the printing style of each Markdown element. The default style is the following:
+```lua
+local color_style = {
+   maxlsz = 80,
+   none = c.none,
+   h1 = c.Magenta,
+   h2 = c.Red,
+   h3 = c.Blue,
+   h4 = c.Cyan,
+   h5 = c.Green,
+   h6 = c.Yellow,
+   blockquote = '',
+   hrule = c.Black,
+   link = c.green,
+   linkcontent = c.Green,
+   code = c.cyan,
+   emph = c.Black,
+   doubleemph = c.Red,
+   tripleemph = c.Magenta,
+   strikethrough = c._white,
+   header = c.White,
+   footer = c.White,
+   image = c.yellow,
+   ulist = c.magenta,
+   olist = c.magenta,
+   tableheader = c.magenta,
+   superscript = '^'
+}
+```
+
+Where colors are ASCII codes defined with:
+```lua
+local c = {
+   none = '\27[0m',
+   black = '\27[0;30m',
+   red = '\27[0;31m',
+   green = '\27[0;32m',
+   yellow = '\27[0;33m',
+   blue = '\27[0;34m',
+   magenta = '\27[0;35m',
+   cyan = '\27[0;36m',
+   white = '\27[0;37m',
+   Black = '\27[1;30m',
+   Red = '\27[1;31m',
+   Green = '\27[1;32m',
+   Yellow = '\27[1;33m',
+   Blue = '\27[1;34m',
+   Magenta = '\27[1;35m',
+   Cyan = '\27[1;36m',
+   White = '\27[1;37m',
+   _black = '\27[40m',
+   _red = '\27[41m',
+   _green = '\27[42m',
+   _yellow = '\27[43m',
+   _blue = '\27[44m',
+   _magenta = '\27[45m',
+   _cyan = '\27[46m',
+   _white = '\27[47m'
+}
+```
+
+You can redefine your own if interested. You can also turn color on/off with the following:
+
+```lua
+local ascii = require 'sundown.ascii'
+
+ascii.bw() -- black and white output
+ascii.render[[
+...
+]]
+
+ascii.color() -- colored output
+ascii.render[[
+...
+]]
+```
+
+# Advanced usage #
+
+All functions from the library `sundown` and `houdini` are accessible through `sundown.C.func` where `func`
+is the function of interest.
+
+See the [Sundown library page](https://github.com/vmg/sundown) for more details.
+
+Note that Houdini C function and structure names are prefixed here with `sd_html_` (e.g. `sd_html_renderer`).
+Sundown C function and structure names are prefixed with `sd_` (e.g. `sd_markdown_render`).
+
+See sdcdefs.lua and htmlcdefs.lua for what is actually available.
diff --git a/ascii.lua b/ascii.lua
new file mode 100644
index 0000000..2375f67
--- /dev/null
+++ b/ascii.lua
@@ -0,0 +1,601 @@
+local sundown = require 'sundown.env'
+local C = sundown.C
+local ffi = require 'ffi'
+local bit = require 'bit'
+
+require 'sundown.sdcdefs'
+
+local c = {
+   none = '\27[0m',
+   black = '\27[0;30m',
+   red = '\27[0;31m',
+   green = '\27[0;32m',
+   yellow = '\27[0;33m',
+   blue = '\27[0;34m',
+   magenta = '\27[0;35m',
+   cyan = '\27[0;36m',
+   white = '\27[0;37m',
+   Black = '\27[1;30m',
+   Red = '\27[1;31m',
+   Green = '\27[1;32m',
+   Yellow = '\27[1;33m',
+   Blue = '\27[1;34m',
+   Magenta = '\27[1;35m',
+   Cyan = '\27[1;36m',
+   White = '\27[1;37m',
+   _black = '\27[40m',
+   _red = '\27[41m',
+   _green = '\27[42m',
+   _yellow = '\27[43m',
+   _blue = '\27[44m',
+   _magenta = '\27[45m',
+   _cyan = '\27[46m',
+   _white = '\27[47m'
+}
+
+local color_style = {
+   maxlsz = 80,
+   none = c.none,
+   h1 = c.Magenta,
+   h2 = c.Red,
+   h3 = c.Blue,
+   h4 = c.Cyan,
+   h5 = c.Green,
+   h6 = c.Yellow,
+   blockquote = '',
+   hrule = c.Black,
+   link = c.green,
+   linkcontent = c.Green,
+   code = c.cyan,
+   emph = c.Black,
+   doubleemph = c.Red,
+   tripleemph = c.Magenta,
+   strikethrough = c._white,
+   header = c.White,
+   footer = c.White,
+   image = c.yellow,
+   ulist = c.magenta,
+   olist = c.magenta,
+   tableheader = c.magenta,
+   superscript = '^'
+}
+
+local bw_style = {
+   maxlsz = 80,
+   none = '',
+   h1 = '',
+   h2 = '',
+   h3 = '',
+   h4 = '',
+   h5 = '',
+   h6 = '',
+   blockquote = '',
+   hrule = '',
+   link = '',
+   linkcontent = '',
+   code = '',
+   emph = '',
+   doubleemph = '',
+   tripleemph = '',
+   strikethrough = '',
+   header = '',
+   footer = '',
+   image = '',
+   ulist = '',
+   olist = '',
+   tableheader = '',
+   superscript = '^'
+}
+
+local default_style = color_style
+
+local function textsize(text)
+   local szt = 0
+   local nw = 0
+   for word in text:gmatch('%S+') do
+      local szw = #word
+      word:gsub('\027%[[%d;]+m',
+                function(stuff)
+                   szw = szw - #stuff
+                end)
+      szt = szt + szw
+      nw = nw+1
+   end
+   if nw > 0 then
+      szt = szt + nw-1
+   end
+   return szt
+end
+
+local function createcallbacks(style)
+   local tree = {}
+   local n = 0
+
+   local callbacks = {
+      blockcode =
+         function(ob, text, lang, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               text = style.code .. text .. style.none
+               n = n+1
+               tree[n] = {tag='blockcode', text=text}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      header =
+         function(ob, text, level, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               level = math.max(math.min(level, 6), 1)
+               text = style['h' .. level] .. text .. style.none
+               n = n+1
+               tree[n] = {tag='header', text=text, level=level}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      blockquote =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               text = style.blockquote .. text .. style.none
+               n = n+1
+               tree[n] = {tag='blockquote', text=text}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      blockhtml =
+         function(ob, text, opaque)
+            -- do nothing
+         end,
+
+      hrule =
+         function(ob, opaque)
+            n = n+1
+            tree[n] = {tag='hrule'}
+            C.sd_bufputs(ob, '\030' .. n .. '\031')
+         end,
+
+      paragraph =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               n = n+1
+               tree[n] = {tag='paragraph', text=text}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      table =
+         function(ob, header, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+            else
+               text = nil
+            end
+
+            if header ~= nil and header.data ~= nil then
+               header = ffi.string(header.data, header.size)
+            else
+               header = nil
+            end
+
+            if text or header then
+               n = n+1
+               tree[n] = {tag='tbl', text=text, header=header}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      table_row =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               n = n+1
+               tree[n] = {tag='tblrow', text=text}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      table_cell =
+         function(ob, text, flags, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               if bit.band(flags, 4) > 0 then
+                  text = style.tableheader .. text .. style.none
+               end
+               flags = bit.band(flags, 3)
+               n = n+1
+               tree[n] = {
+                  tag='tblcell',
+                  text=text,
+                  size=textsize(text),
+                  left=(flags==1),
+                  right=(flags==2),
+                  center=(flags==3)
+               }
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      list =
+         function(ob, text, flags, opaque)
+            if text and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               n = n+1
+               tree[n] = {tag='list', text=text, type=bit.band(flags, 1)}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      listitem =
+         function(ob, text, flags, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               n = n+1
+               tree[n] = {tag='listitem', text=text}
+               C.sd_bufputs(ob, '\030' .. n .. '\031')
+            end
+         end,
+
+      normal_text =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               text = text:gsub('[\029\030\031]', '')
+               C.sd_bufputs(ob, text)
+            end
+         end,
+
+      entity =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               text = text:gsub('[\029\030\031]', '')
+               C.sd_bufputs(ob, text)
+            end
+         end,
+
+      autolink =
+         function(ob, link, ltype, opaque)
+            if link ~= nil and link.data ~= nil then
+               link = ffi.string(link.data, link.size)
+               link = style.link .. link .. style.none
+               C.sd_bufputs(ob, link)
+            end
+            return 1
+         end,
+
+      codespan =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = ffi.string(text.data, text.size)
+               text = style.code .. text .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      double_emphasis =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = style.doubleemph .. ffi.string(text.data, text.size) .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      emphasis =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = style.emph .. ffi.string(text.data, text.size) .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      image =
+         function(ob, link, title, alt, opaque)
+            local text = style.image .. '[image: '
+            if title ~= nil and title.data ~= nil then
+               text = text .. ffi.string(title.data, title.size)
+            elseif alt ~= nil and alt.data ~= nil then
+               text = text .. ffi.string(alt.data, alt.size)
+            elseif link ~= nil and link.data ~= nil then
+               text = text .. ffi.string(link.data, link.size)
+            end
+            text = text .. ']' .. style.none
+            C.sd_bufputs(ob, text)
+            return 1
+         end,
+
+      linebreak =
+         function(ob, opaque)
+            local text = '\029'
+            C.sd_bufputs(ob, text)
+         end,
+
+      link =
+         function(ob, link, title, content, opaque)
+            local text = ''
+            if content ~= nil and content.data ~= nil then
+               text = style.linkcontent .. ffi.string(content.data, content.size) .. style.none
+            end
+            if link ~= nil and link.data ~= nil then
+               local link = ffi.string(link.data, link.size)
+               if not link:match('^#') then
+                  text = text .. ' ' .. style.link .. '[' .. link .. ']' .. style.none
+               end
+            end
+            if #text > 0 then
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      raw_html_tag =
+         function(ob, tag, opaque)
+            -- just ignore it
+            return 1
+         end,
+
+      triple_emphasis =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = style.tripleemph .. ffi.string(text.data, text.size) .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      strikethrough =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = style.strikethrough .. ffi.string(text.data, text.size) .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      superscript =
+         function(ob, text, opaque)
+            if text ~= nil and text.data ~= nil then
+               text = style.superscript .. ffi.string(text.data, text.size) .. style.none
+               C.sd_bufputs(ob, text)
+            end
+            return 1
+         end,
+
+      doc_header =
+         function(ob, opaque)
+         end,
+
+      doc_footer =
+         function(ob, opaque)
+         end
+   }
+
+   return callbacks, tree
+end
+
+
+local function preprocess(txt, style)
+   local callbacks, tree = createcallbacks(style)
+   local c_callbacks = ffi.new('struct sd_callbacks', callbacks)
+   local options = nil
+   local markdown = C.sd_markdown_new(0xfff, 16, c_callbacks, options)
+
+   local outbuf = C.sd_bufnew(64)
+
+   C.sd_markdown_render(outbuf, ffi.cast('const char*', txt), #txt, markdown)
+   C.sd_markdown_free(markdown)
+
+   for name,_ in pairs(callbacks) do
+      c_callbacks[name]:free()
+   end
+
+   txt = ffi.string(outbuf.data, outbuf.size)
+   C.sd_bufrelease(outbuf)
+
+   return txt, tree
+end
+
+local function showindent(out, text, indent)
+   for line, brk in text:gmatch('([^\n]+)([\n]*)') do
+      table.insert(out, string.rep(' ', indent) .. line)
+      for i=1,#brk-1 do
+         table.insert(out, '')
+      end
+   end
+end
+
+local function showjustified(out, text, indent, maxlsz)
+   local lines = {}
+   local szl = 0
+   local line = {}
+
+   local function newline()
+      if #line > 0 then
+         table.insert(lines, string.rep(' ', indent) .. table.concat(line, ' '))
+         line = {}
+         szl = 0
+      end
+   end
+
+   for word in text:gmatch('%S+') do
+      local szw = #word
+      word:gsub('\027%[[%d;]+m',
+                function(stuff)
+                   szw = szw - #stuff
+                end)
+
+      if szl+szw+1 > maxlsz-indent then
+         newline()
+      end
+      table.insert(line, word)
+      szl = szl + szw+1
+   end
+   newline()
+
+   table.insert(out, table.concat(lines, '\n'))
+end
+
+local function show(out, txt, tree, indent, style, maxlsz)
+   maxlsz = maxlsz or style.maxlsz
+
+   local idx = 1
+   local node
+
+   while true do
+      local i, j
+      i, j = txt:find('[^\029\030\031]+', idx)
+      if i and i == idx then
+         showjustified(out, txt:sub(i, j), indent, maxlsz)
+         idx = j+1
+      else
+         i, j = txt:find('\029', idx)
+         if i and i == idx then
+            table.insert(out, '')
+            idx = j+1
+         else
+            i, j = txt:find('(%b\030\031)', idx)
+            if i and i == idx then
+               idx = j+1
+               local node = tree[ tonumber(txt:sub(i+1, j-1)) ]
+               if node.tag == 'blockcode' then
+                  table.insert(out, '')
+                  showindent(out, node.text, indent)
+               elseif node.tag == 'blockquote' then
+                  table.insert(out, '')
+                  show(out, node.text, tree, indent+5, style, maxlsz-5)
+               elseif node.tag == 'header' then
+                  table.insert(out, '')
+                  indent = node.level
+                  showindent(out, style['h' .. node.level] .. string.rep('+', maxlsz-indent+1) .. style.none, indent-1)
+                  showjustified(out, node.text, indent-1, maxlsz)
+               elseif node.tag == 'hrule' then
+                  table.insert(out, '')
+                  showindent(out, style.hrule .. string.rep('_', maxlsz-indent) .. style.none, indent)
+               elseif node.tag == 'paragraph' then
+                  table.insert(out, '')
+                  showjustified(out, node.text, indent, maxlsz, style)
+               elseif node.tag == 'list' then
+                  if node.type == 0 then
+                     for nidx in node.text:gmatch('(%b\030\031)') do
+                        local subnode = tree[ tonumber(nidx:sub(2, -2)) ]
+                        while subnode.text:match('^(%b\030\031)') do
+                           subnode = tree[ tonumber( subnode.text:match('^(%b\030\031)'):sub(2, -2) ) ]
+                        end
+                        subnode.text = style.ulist .. '* ' .. style.none .. subnode.text
+                     end
+                  else
+                     local oidx = 0
+                     for nidx in node.text:gmatch('(%b\030\031)') do
+                        local subnode = tree[ tonumber(nidx:sub(2, -2)) ]
+                        while subnode.text:match('^(%b\030\031)') do
+                           subnode = tree[ tonumber( subnode.text:match('^(%b\030\031)'):sub(2, -2) ) ]
+                        end
+                        oidx = oidx + 1
+                        subnode.text = style.olist .. oidx .. '. ' .. style.none .. subnode.text
+                     end
+                  end
+                  table.insert(out, '')
+                  show(out, node.text, tree, indent+3, style, maxlsz)
+                  table.insert(out, '')
+               elseif node.tag == 'listitem' then
+                  show(out, node.text, tree, indent, style, maxlsz)
+               elseif node.tag == 'tbl' then
+                  -- find cell sizes
+                  local function rendertblsz(text, maxsz)
+                     local idxrow = 0
+                     for row in text:gmatch('(%b\030\031)') do
+                        idxrow = idxrow + 1
+                        local sz = {}
+                        row = tree[ tonumber(row:sub(2, -2)) ]
+                        assert(row.tag == 'tblrow')
+                        local idxcell = 0
+                        for cell in row.text:gmatch('(%b\030\031)') do
+                           idxcell = idxcell + 1
+                           sz[idxcell] = sz[idxcell] or 0
+                           maxsz[idxcell] = maxsz[idxcell] or 0
+                           cell = tree[ tonumber(cell:sub(2, -2)) ]
+                           assert(cell.tag == 'tblcell')
+                           sz[idxcell] = sz[idxcell] + cell.size
+                        end
+                        for idxcell=1,#sz do
+                           maxsz[idxcell] = math.max(maxsz[idxcell], sz[idxcell])
+                        end
+                     end
+                  end
+
+                  local maxsz = {}
+                  rendertblsz(node.header, maxsz)
+                  rendertblsz(node.text, maxsz)
+
+                  -- print it
+                  local function rendertbl(text, maxsz, isheader)
+                     local sztot = 0
+                     for i=1,#maxsz do
+                        sztot = sztot + maxsz[i]
+                     end
+                     local idxrow = 0
+                     if isheader then
+                        showindent(out, ' ' .. string.rep('-', sztot+(#maxsz-1)*3+2), indent)
+                     end
+                     for row in text:gmatch('(%b\030\031)') do
+                        idxrow = idxrow + 1
+                        row = tree[ tonumber(row:sub(2, -2)) ]
+                        local line = {}
+                        local idxcell = 0
+                        for cell in row.text:gmatch('(%b\030\031)') do
+                           idxcell = idxcell + 1
+                           cell = tree[ tonumber(cell:sub(2, -2)) ]
+                           if cell.right then
+                              table.insert(line, string.rep(' ', maxsz[idxcell]-cell.size) .. cell.text)
+                           elseif cell.center then
+                              local szh2 = math.floor((maxsz[idxcell]-cell.size)/2)
+                              table.insert(line, string.rep(' ', szh2) .. cell.text .. string.rep(' ', maxsz[idxcell]-cell.size-szh2))
+                           else
+                              table.insert(line, cell.text .. string.rep(' ', maxsz[idxcell]-cell.size))
+                           end
+                        end
+                        showindent(out, '| ' .. table.concat(line, ' | ') .. ' |', indent)
+                     end
+                     showindent(out, ' ' .. string.rep('-', sztot+(#maxsz-1)*3+2), indent)
+                  end
+                  rendertbl(node.header, maxsz, true)
+                  rendertbl(node.text, maxsz)
+               end
+            else
+               break
+            end
+         end
+      end
+   end
+end
+
+local function render(txt, style)
+   local tree
+   local out = {}
+
+   style = style or default_style
+   txt, tree = preprocess(txt, style)
+
+   show(out, txt, tree, 0, style)
+
+   return table.concat(out, '\n')
+end
+
+local function color()
+   default_style = color_style
+   return default_style
+end
+
+local function bw()
+   default_style = bw_style
+   return default_style
+end
+
+return {render=render, bw=bw, color=color}
diff --git a/env.lua b/env.lua
new file mode 100644
index 0000000..37be420
--- /dev/null
+++ b/env.lua
@@ -0,0 +1,20 @@
+local ffi = require 'ffi'
+
+local sundown = {}
+
+-- Compat function from https://github.com/stevedonovan/Penlight/blob/master/lua/pl/compat.lua
+if not package.searchpath then
+   local sep = package.config:sub(1,1)
+   function package.searchpath (mod,path)
+      mod = mod:gsub('%.',sep)
+      for m in path:gmatch('[^;]+') do
+         local nm = m:gsub('?',mod)
+         local f = io.open(nm,'r')
+         if f then f:close(); return nm end
+        end
+    end
+end
+
+sundown.C = ffi.load(package.searchpath('libsundown', package.cpath))
+
+return sundown
diff --git a/html.lua b/html.lua
new file mode 100644
index 0000000..349cfec
--- /dev/null
+++ b/html.lua
@@ -0,0 +1,23 @@
+local sundown = require 'sundown.env'
+local ffi = require 'ffi'
+local C = sundown.C
+
+require 'sundown.sdcdefs'
+require 'sundown.htmlcdefs'
+
+local function render(txt)
+   local callbacks = ffi.new('struct sd_callbacks')
+   local options = ffi.new('struct sd_html_renderopt')
+   C.sd_html_renderer(callbacks, options, 0)
+   local markdown = C.sd_markdown_new(0xfff, 16, callbacks, options)
+
+   local outbuf = C.sd_bufnew(64)
+   C.sd_markdown_render(outbuf, ffi.cast('const char*', txt), #txt, markdown)
+   C.sd_markdown_free(markdown)
+   txt = ffi.string(outbuf.data, outbuf.size)
+   C.sd_bufrelease(outbuf)
+
+   return txt
+end
+
+return {render=render}
diff --git a/html/houdini.h b/html/houdini.h
new file mode 100644
index 0000000..6fbecc3
--- /dev/null
+++ b/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 sd_houdini_escape_html(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_html0(struct sd_buf *ob, const uint8_t *src, size_t size, int secure);
+extern void sd_houdini_unescape_html(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_xml(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_uri(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_url(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_href(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_unescape_uri(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_unescape_url(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_escape_js(struct sd_buf *ob, const uint8_t *src, size_t size);
+extern void sd_houdini_unescape_js(struct sd_buf *ob, const uint8_t *src, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/html/houdini_href_e.c b/html/houdini_href_e.c
new file mode 100644
index 0000000..539bbfe
--- /dev/null
+++ b/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
+sd_houdini_escape_href(struct sd_buf *ob, const uint8_t *src, size_t size)
+{
+	static const char hex_chars[] = "0123456789ABCDEF";
+	size_t  i = 0, org;
+	char hex_str[3];
+
+	sd_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)
+			sd_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, "&");
+			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 ' ':
+			sd_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];
+			sd_bufput(ob, hex_str, 3);
+		}
+
+		i++;
+	}
+}
diff --git a/html/houdini_html_e.c b/html/houdini_html_e.c
new file mode 100644
index 0000000..52a1cd0
--- /dev/null
+++ b/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:
+ *
+ * & --> &
+ * < --> <
+ * > --> >
+ * " --> "
+ * ' --> &#x27;     ' 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[] = {
+        "",
+        """,
+        "&",
+        "'",
+        "/",
+        "<",
+        ">"
+};
+
+void
+sd_houdini_escape_html0(struct sd_buf *ob, const uint8_t *src, size_t size, int secure)
+{
+	size_t i = 0, org, esc = 0;
+
+	sd_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)
+			sd_bufput(ob, src + org, i - org);
+
+		/* escaping */
+		if (i >= size)
+			break;
+
+		/* The forward slash is only escaped in secure mode */
+		if (src[i] == '/' && !secure) {
+			sd_bufputc(ob, '/');
+		} else {
+			sd_bufputs(ob, HTML_ESCAPES[esc]);
+		}
+
+		i++;
+	}
+}
+
+void
+sd_houdini_escape_html(struct sd_buf *ob, const uint8_t *src, size_t size)
+{
+	sd_houdini_escape_html0(ob, src, size, 1);
+}
+
diff --git a/html/html.c b/html/html.c
new file mode 100644
index 0000000..a9c6660
--- /dev/null
+++ b/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
+sd_html_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 sd_buf *ob, const uint8_t *source, size_t length)
+{
+	sd_houdini_escape_html0(ob, source, length, 0);
+}
+
+static inline void escape_href(struct sd_buf *ob, const uint8_t *source, size_t length)
+{
+	sd_houdini_escape_href(ob, source, length);
+}
+
+/********************
+ * GENERIC RENDERER *
+ ********************/
+static int
+rndr_autolink(struct sd_buf *ob, const struct sd_buf *link, enum sd_mkd_autolink type, void *opaque)
+{
+	struct sd_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) {
+		sd_bufputc(ob, '\"');
+		options->link_attributes(ob, link, opaque);
+		sd_bufputc(ob, '>');
+	} else {
+		BUFPUTSL(ob, "\">");
+	}
+
+	/*
+	 * Pretty printing: if we get an email address as
+	 * an actual URI, e.g. `mailto:foo at bar.com`, we don't
+	 * want to print the `mailto:` prefix
+	 */
+	if (sd_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 sd_buf *ob, const struct sd_buf *text, const struct sd_buf *lang, void *opaque)
+{
+	if (ob->size) sd_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) sd_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 sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (ob->size) sd_bufputc(ob, '\n');
+	BUFPUTSL(ob, "<blockquote>\n");
+	if (text) sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</blockquote>\n");
+}
+
+static int
+rndr_codespan(struct sd_buf *ob, const struct sd_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 sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (!text || !text->size)
+		return 0;
+
+	BUFPUTSL(ob, "<del>");
+	sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</del>");
+	return 1;
+}
+
+static int
+rndr_double_emphasis(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (!text || !text->size)
+		return 0;
+
+	BUFPUTSL(ob, "<strong>");
+	sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</strong>");
+
+	return 1;
+}
+
+static int
+rndr_emphasis(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<em>");
+	if (text) sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</em>");
+	return 1;
+}
+
+static int
+rndr_linebreak(struct sd_buf *ob, void *opaque)
+{
+	struct sd_html_renderopt *options = opaque;
+	sd_bufputs(ob, USE_XHTML(options) ? "<br/>\n" : "<br>\n");
+	return 1;
+}
+
+static void
+rndr_header(struct sd_buf *ob, const struct sd_buf *text, int level, void *opaque)
+{
+	struct sd_html_renderopt *options = opaque;
+
+	if (ob->size)
+		sd_bufputc(ob, '\n');
+
+	if (options->flags & HTML_TOC)
+		sd_bufprintf(ob, "<h%d id=\"toc_%d\">", level, options->toc_data.header_count++);
+	else
+		sd_bufprintf(ob, "<h%d>", level);
+
+	if (text) sd_bufput(ob, text->data, text->size);
+	sd_bufprintf(ob, "</h%d>\n", level);
+}
+
+static int
+rndr_link(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *content, void *opaque)
+{
+	struct sd_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) {
+		sd_bufputc(ob, '\"');
+		options->link_attributes(ob, link, opaque);
+		sd_bufputc(ob, '>');
+	} else {
+		BUFPUTSL(ob, "\">");
+	}
+
+	if (content && content->size) sd_bufput(ob, content->data, content->size);
+	BUFPUTSL(ob, "</a>");
+	return 1;
+}
+
+static void
+rndr_list(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque)
+{
+	if (ob->size) sd_bufputc(ob, '\n');
+	sd_bufput(ob, flags & MKD_LIST_ORDERED ? "<ol>\n" : "<ul>\n", 5);
+	if (text) sd_bufput(ob, text->data, text->size);
+	sd_bufput(ob, flags & MKD_LIST_ORDERED ? "</ol>\n" : "</ul>\n", 6);
+}
+
+static void
+rndr_listitem(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque)
+{
+	BUFPUTSL(ob, "<li>");
+	if (text) {
+		size_t size = text->size;
+		while (size && text->data[size - 1] == '\n')
+			size--;
+
+		sd_bufput(ob, text->data, size);
+	}
+	BUFPUTSL(ob, "</li>\n");
+}
+
+static void
+rndr_paragraph(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	struct sd_html_renderopt *options = opaque;
+	size_t i = 0;
+
+	if (ob->size) sd_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)
+				sd_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 {
+		sd_bufput(ob, &text->data[i], text->size - i);
+	}
+	BUFPUTSL(ob, "</p>\n");
+}
+
+static void
+rndr_raw_block(struct sd_buf *ob, const struct sd_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) sd_bufputc(ob, '\n');
+	sd_bufput(ob, text->data + org, sz - org);
+	sd_bufputc(ob, '\n');
+}
+
+static int
+rndr_triple_emphasis(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<strong><em>");
+	sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</em></strong>");
+	return 1;
+}
+
+static void
+rndr_hrule(struct sd_buf *ob, void *opaque)
+{
+	struct sd_html_renderopt *options = opaque;
+	if (ob->size) sd_bufputc(ob, '\n');
+	sd_bufputs(ob, USE_XHTML(options) ? "<hr/>\n" : "<hr>\n");
+}
+
+static int
+rndr_image(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *alt, void *opaque)
+{
+	struct sd_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); }
+
+	sd_bufputs(ob, USE_XHTML(options) ? "\"/>" : "\">");
+	return 1;
+}
+
+static int
+rndr_raw_html(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	struct sd_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 &&
+		sd_html_is_tag(text->data, text->size, "style"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_LINKS) != 0 &&
+		sd_html_is_tag(text->data, text->size, "a"))
+		return 1;
+
+	if ((options->flags & HTML_SKIP_IMAGES) != 0 &&
+		sd_html_is_tag(text->data, text->size, "img"))
+		return 1;
+
+	sd_bufput(ob, text->data, text->size);
+	return 1;
+}
+
+static void
+rndr_table(struct sd_buf *ob, const struct sd_buf *header, const struct sd_buf *body, void *opaque)
+{
+	if (ob->size) sd_bufputc(ob, '\n');
+	BUFPUTSL(ob, "<table><thead>\n");
+	if (header)
+		sd_bufput(ob, header->data, header->size);
+	BUFPUTSL(ob, "</thead><tbody>\n");
+	if (body)
+		sd_bufput(ob, body->data, body->size);
+	BUFPUTSL(ob, "</tbody></table>\n");
+}
+
+static void
+rndr_tablerow(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	BUFPUTSL(ob, "<tr>\n");
+	if (text)
+		sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</tr>\n");
+}
+
+static void
+rndr_tablecell(struct sd_buf *ob, const struct sd_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)
+		sd_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 sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (!text || !text->size) return 0;
+	BUFPUTSL(ob, "<sup>");
+	sd_bufput(ob, text->data, text->size);
+	BUFPUTSL(ob, "</sup>");
+	return 1;
+}
+
+static void
+rndr_normal_text(struct sd_buf *ob, const struct sd_buf *text, void *opaque)
+{
+	if (text)
+		escape_html(ob, text->data, text->size);
+}
+
+static void
+toc_header(struct sd_buf *ob, const struct sd_buf *text, int level, void *opaque)
+{
+	struct sd_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");
+	}
+
+	sd_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 sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *content, void *opaque)
+{
+	if (content && content->size)
+		sd_bufput(ob, content->data, content->size);
+	return 1;
+}
+
+static void
+toc_finalize(struct sd_buf *ob, void *opaque)
+{
+	struct sd_html_renderopt *options = opaque;
+
+	while (options->toc_data.current_level > 0) {
+		BUFPUTSL(ob, "</li>\n</ul>\n");
+		options->toc_data.current_level--;
+	}
+}
+
+void
+sd_html_toc_renderer(struct sd_callbacks *callbacks, struct sd_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 sd_html_renderopt));
+	options->flags = HTML_TOC;
+
+	memcpy(callbacks, &cb_default, sizeof(struct sd_callbacks));
+}
+
+void
+sd_html_renderer(struct sd_callbacks *callbacks, struct sd_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 sd_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/html/html.h b/html/html.h
new file mode 100644
index 0000000..5152caf
--- /dev/null
+++ b/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 sd_html_renderopt {
+	struct {
+		int header_count;
+		int current_level;
+		int level_offset;
+	} toc_data;
+
+	unsigned int flags;
+
+	/* extra callbacks */
+	void (*link_attributes)(struct sd_buf *ob, const struct sd_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),
+} sd_html_render_mode;
+
+typedef enum {
+	HTML_TAG_NONE = 0,
+	HTML_TAG_OPEN,
+	HTML_TAG_CLOSE,
+} html_tag;
+
+int
+sd_html_is_tag(const uint8_t *tag_data, size_t tag_size, const char *tagname);
+
+extern void
+sd_html_renderer(struct sd_callbacks *callbacks, struct sd_html_renderopt *options_ptr, unsigned int render_flags);
+
+extern void
+sd_html_toc_renderer(struct sd_callbacks *callbacks, struct sd_html_renderopt *options_ptr);
+
+extern void
+sd_html_smartypants(struct sd_buf *ob, const uint8_t *text, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
diff --git a/html/html_smartypants.c b/html/html_smartypants.c
new file mode 100644
index 0000000..d7de747
--- /dev/null
+++ b/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 sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__dquote(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__amp(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__period(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__number(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__dash(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__parens(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__squote(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__backtick(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+static size_t smartypants_cb__escape(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
+
+static size_t (*smartypants_cb_ptrs[])
+	(struct sd_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 sd_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);
+	sd_bufputs(ob, ent);
+	return 1;
+}
+
+static size_t
+smartypants_cb__squote(struct sd_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, "’");
+			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, "’");
+				return 0;
+			}
+		}
+	}
+
+	if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote))
+		return 0;
+
+	sd_bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__parens(struct sd_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, "©");
+			return 2;
+		}
+
+		if (t1 == 'r' && t2 == ')') {
+			BUFPUTSL(ob, "®");
+			return 2;
+		}
+
+		if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') {
+			BUFPUTSL(ob, "™");
+			return 3;
+		}
+	}
+
+	sd_bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__dash(struct sd_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, "—");
+		return 2;
+	}
+
+	if (size >= 2 && text[1] == '-') {
+		BUFPUTSL(ob, "–");
+		return 1;
+	}
+
+	sd_bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__amp(struct sd_buf *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
+{
+	if (size >= 6 && memcmp(text, """, 6) == 0) {
+		if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote))
+			return 5;
+	}
+
+	if (size >= 4 && memcmp(text, "�", 4) == 0)
+		return 3;
+
+	sd_bufputc(ob, '&');
+	return 0;
+}
+
+static size_t
+smartypants_cb__period(struct sd_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, "…");
+		return 2;
+	}
+
+	if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') {
+		BUFPUTSL(ob, "…");
+		return 4;
+	}
+
+	sd_bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__backtick(struct sd_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 sd_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;
+			}
+		}
+	}
+
+	sd_bufputc(ob, text[0]);
+	return 0;
+}
+
+static size_t
+smartypants_cb__dquote(struct sd_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, """);
+
+	return 0;
+}
+
+static size_t
+smartypants_cb__ltag(struct sd_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 (sd_html_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 (sd_html_is_tag(text + i, size - i, skip_tags[tag]) == HTML_TAG_CLOSE)
+				break;
+
+			i++;
+		}
+
+		while (i < size && text[i] != '>')
+			i++;
+	}
+
+	sd_bufput(ob, text, i + 1);
+	return i;
+}
+
+static size_t
+smartypants_cb__escape(struct sd_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 '`':
+		sd_bufputc(ob, text[1]);
+		return 1;
+
+	default:
+		sd_bufputc(ob, '\\');
+		return 0;
+	}
+}
+
+#if 0
+static struct {
+    uint8_t c0;
+    const uint8_t *pattern;
+    const uint8_t *entity;
+    int skip;
+} smartypants_subs[] = {
+    { '\'', "'s>",      "’",  0 },
+    { '\'', "'t>",      "’",  0 },
+    { '\'', "'re>",     "’",  0 },
+    { '\'', "'ll>",     "’",  0 },
+    { '\'', "'ve>",     "’",  0 },
+    { '\'', "'m>",      "’",  0 },
+    { '\'', "'d>",      "’",  0 },
+    { '-',  "--",       "—",  1 },
+    { '-',  "<->",      "–",  0 },
+    { '.',  "...",      "…", 2 },
+    { '.',  ". . .",    "…", 4 },
+    { '(',  "(c)",      "©",   2 },
+    { '(',  "(r)",      "®",    2 },
+    { '(',  "(tm)",     "™",  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,       3 },
+};
+#endif
+
+void
+sd_html_smartypants(struct sd_buf *ob, const uint8_t *text, size_t size)
+{
+	size_t i;
+	struct smartypants_data smrt = {0, 0};
+
+	if (!text)
+		return;
+
+	sd_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)
+			sd_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);
+		}
+	}
+}
+
+
diff --git a/htmlcdefs.lua b/htmlcdefs.lua
new file mode 100644
index 0000000..9b2f9e9
--- /dev/null
+++ b/htmlcdefs.lua
@@ -0,0 +1,45 @@
+local ffi = require 'ffi'
+
+ffi.cdef[[
+
+struct sd_html_renderopt {
+	struct {
+		int header_count;
+		int current_level;
+		int level_offset;
+	} toc_data;
+
+	unsigned int flags;
+
+	/* extra callbacks */
+	void (*link_attributes)(struct sd_buf *ob, const struct sd_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),
+} sd_html_render_mode;
+
+typedef enum {
+	HTML_TAG_NONE = 0,
+	HTML_TAG_OPEN,
+	HTML_TAG_CLOSE,
+} sd_html_tag;
+
+int sd_html_is_tag(const uint8_t *tag_data, size_t tag_size, const char *tagname);
+
+void sd_html_renderer(struct sd_callbacks *callbacks, struct sd_html_renderopt *options_ptr, unsigned int render_flags);
+
+void sd_html_toc_renderer(struct sd_callbacks *callbacks, struct sd_html_renderopt *options_ptr);
+
+void sd_html_smartypants(struct sd_buf *ob, const uint8_t *text, size_t size);
+
+]]
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..52eb393
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,9 @@
+local sundown = require 'sundown.env'
+local html = require 'sundown.html'
+local ascii = require 'sundown.ascii'
+
+sundown.render = html.render
+sundown.renderHTML = html.render
+sundown.renderASCII = ascii.render
+
+return sundown
diff --git a/mdcat.lua b/mdcat.lua
new file mode 100644
index 0000000..c3f075b
--- /dev/null
+++ b/mdcat.lua
@@ -0,0 +1,5 @@
+#!/usr/bin/env lua
+
+local ascii = require 'sundown.ascii'
+assert(#arg == 1, 'usage: mdcat <file.md>')
+print(ascii.render(io.open(arg[1]):read('*all')))
diff --git a/rocks/sundown-scm-1.rockspec b/rocks/sundown-scm-1.rockspec
new file mode 100644
index 0000000..586c5a4
--- /dev/null
+++ b/rocks/sundown-scm-1.rockspec
@@ -0,0 +1,51 @@
+package = "sundown"
+version = "scm-1"
+
+source = {
+   url = "git://github.com/torch/sundown-ffi.git"
+}
+
+description = {
+   summary = "A FFI interface to the Markdown implementation of the Sundown library",
+   detailed = [[
+   ]],
+   homepage = "https://github.com/torch/sundown-ffi",
+   license = "BSD"
+}
+
+dependencies = {
+   "lua >= 5.1",
+}
+
+build = {
+   type = "builtin",
+   install = {
+      bin = {
+         mdcat = "mdcat.lua"
+      }
+   },
+   modules = {
+      ["sundown.env"] = "env.lua",
+      ["sundown.init"] = "init.lua",
+      ["sundown.sdcdefs"] = "sdcdefs.lua",
+      ["sundown.htmlcdefs"] = "htmlcdefs.lua",
+      ["sundown.html"] = "html.lua",
+      ["sundown.ascii"] = "ascii.lua",
+      libsundown = {
+         sources = {
+            "src/autolink.c",
+            "src/buffer.c",
+            "src/markdown.c",
+            "src/stack.c",
+            "html/houdini_href_e.c",
+            "html/houdini_html_e.c",
+            "html/html.c",
+            "html/html_smartypants.c"
+         },
+         incdirs = {
+            "src/",
+            "html/"
+         }
+      }
+   }
+}
diff --git a/sdcdefs.lua b/sdcdefs.lua
new file mode 100644
index 0000000..0afbf85
--- /dev/null
+++ b/sdcdefs.lua
@@ -0,0 +1,96 @@
+local ffi = require 'ffi'
+
+ffi.cdef[[
+
+struct sd_buf {
+	uint8_t *data;
+	size_t size;
+	size_t asize;
+	size_t unit;
+};
+
+enum sd_mkd_autolink {
+	MKDA_NOT_AUTOLINK,
+	MKDA_NORMAL,
+	MKDA_EMAIL,
+};
+
+enum sd_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 sd_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_SPACE_HEADERS = (1 << 6),
+	MKDEXT_SUPERSCRIPT = (1 << 7),
+	MKDEXT_LAX_SPACING = (1 << 8),
+};
+
+struct sd_callbacks {
+	void (*blockcode)(struct sd_buf *ob, const struct sd_buf *text, const struct sd_buf *lang, void *opaque);
+	void (*blockquote)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*blockhtml)(struct sd_buf *ob,const  struct sd_buf *text, void *opaque);
+	void (*header)(struct sd_buf *ob, const struct sd_buf *text, int level, void *opaque);
+	void (*hrule)(struct sd_buf *ob, void *opaque);
+	void (*list)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+	void (*listitem)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+	void (*paragraph)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*table)(struct sd_buf *ob, const struct sd_buf *header, const struct sd_buf *body, void *opaque);
+	void (*table_row)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*table_cell)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+
+	int (*autolink)(struct sd_buf *ob, const struct sd_buf *link, enum sd_mkd_autolink type, void *opaque);
+	int (*codespan)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*double_emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*image)(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *alt, void *opaque);
+	int (*linebreak)(struct sd_buf *ob, void *opaque);
+	int (*link)(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *content, void *opaque);
+	int (*raw_html_tag)(struct sd_buf *ob, const struct sd_buf *tag, void *opaque);
+	int (*triple_emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*strikethrough)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*superscript)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+
+	void (*entity)(struct sd_buf *ob, const struct sd_buf *entity, void *opaque);
+	void (*normal_text)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+
+	void (*doc_header)(struct sd_buf *ob, void *opaque);
+	void (*doc_footer)(struct sd_buf *ob, void *opaque);
+};
+
+struct sd_markdown;
+
+struct sd_markdown * sd_markdown_new(
+	unsigned int extensions,
+	size_t max_nesting,
+	const struct sd_callbacks *callbacks,
+	void *opaque);
+
+void sd_markdown_render(struct sd_buf *ob, const uint8_t *document, size_t doc_size, struct sd_markdown *md);
+
+void sd_markdown_free(struct sd_markdown *md);
+
+void sd_version(int *major, int *minor, int *revision);
+
+/* buffer */
+
+int sd_bufgrow(struct sd_buf *, size_t);
+struct sd_buf *sd_bufnew(size_t) __attribute__ ((malloc));
+const char *sd_bufcstr(struct sd_buf *);
+int sd_bufprefix(const struct sd_buf *buf, const char *prefix);
+void sd_bufput(struct sd_buf *, const void *, size_t);
+void sd_bufputs(struct sd_buf *, const char *);
+void sd_bufputc(struct sd_buf *, int);
+void sd_bufrelease(struct sd_buf *);
+void sd_bufreset(struct sd_buf *);
+void sd_bufslurp(struct sd_buf *, size_t);
+
+]]
diff --git a/src/autolink.c b/src/autolink.c
new file mode 100644
index 0000000..5359497
--- /dev/null
+++ b/src/autolink.c
@@ -0,0 +1,297 @@
+/*
+ * 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 "autolink.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#if defined(_WIN32)
+#define strncasecmp	_strnicmp
+#endif
+
+int
+sd_autolink_issafe(const uint8_t *link, size_t link_len)
+{
+	static const size_t valid_uris_count = 5;
+	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((char *)link, valid_uris[i], len) == 0 &&
+			isalnum(link[len]))
+			return 1;
+	}
+
+	return 0;
+}
+
+static size_t
+autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size)
+{
+	uint8_t 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(uint8_t *data, size_t size, int allow_short)
+{
+	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 (allow_short) {
+		/* We don't need a valid domain in the strict sense (with
+		 * least one dot; so just make sure it's composed of valid
+		 * domain characters and return the length of the the valid
+		 * sequence. */
+		return i;
+	} else {
+		/* a valid domain needs to have at least a dot.
+		 * that's as far as we get */
+		return np ? i : 0;
+	}
+}
+
+size_t
+sd_autolink__www(
+	size_t *rewind_p,
+	struct sd_buf *link,
+	uint8_t *data,
+	size_t max_rewind,
+	size_t size,
+	unsigned int flags)
+{
+	size_t link_end;
+
+	if (max_rewind > 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, 0);
+
+	if (link_end == 0)
+		return 0;
+
+	while (link_end < size && !isspace(data[link_end]))
+		link_end++;
+
+	link_end = autolink_delim(data, link_end, max_rewind, size);
+
+	if (link_end == 0)
+		return 0;
+
+	sd_bufput(link, data, link_end);
+	*rewind_p = 0;
+
+	return (int)link_end;
+}
+
+size_t
+sd_autolink__email(
+	size_t *rewind_p,
+	struct sd_buf *link,
+	uint8_t *data,
+	size_t max_rewind,
+	size_t size,
+	unsigned int flags)
+{
+	size_t link_end, rewind;
+	int nb = 0, np = 0;
+
+	for (rewind = 0; rewind < max_rewind; ++rewind) {
+		uint8_t 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) {
+		uint8_t 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 ||
+		!isalpha(data[link_end - 1]))
+		return 0;
+
+	link_end = autolink_delim(data, link_end, max_rewind, size);
+
+	if (link_end == 0)
+		return 0;
+
+	sd_bufput(link, data - rewind, link_end + rewind);
+	*rewind_p = rewind;
+
+	return link_end;
+}
+
+size_t
+sd_autolink__url(
+	size_t *rewind_p,
+	struct sd_buf *link,
+	uint8_t *data,
+	size_t max_rewind,
+	size_t size,
+	unsigned int flags)
+{
+	size_t link_end, rewind = 0, domain_len;
+
+	if (size < 4 || data[1] != '/' || data[2] != '/')
+		return 0;
+
+	while (rewind < max_rewind && 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,
+		flags & SD_AUTOLINK_SHORT_DOMAINS);
+
+	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, max_rewind, size);
+
+	if (link_end == 0)
+		return 0;
+
+	sd_bufput(link, data - rewind, link_end + rewind);
+	*rewind_p = rewind;
+
+	return link_end;
+}
+
diff --git a/src/autolink.h b/src/autolink.h
new file mode 100644
index 0000000..dc5b723
--- /dev/null
+++ b/src/autolink.h
@@ -0,0 +1,51 @@
+/*
+ * 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"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum {
+	SD_AUTOLINK_SHORT_DOMAINS = (1 << 0),
+};
+
+int
+sd_autolink_issafe(const uint8_t *link, size_t link_len);
+
+size_t
+sd_autolink__www(size_t *rewind_p, struct sd_buf *link,
+	uint8_t *data, size_t offset, size_t size, unsigned int flags);
+
+size_t
+sd_autolink__email(size_t *rewind_p, struct sd_buf *link,
+	uint8_t *data, size_t offset, size_t size, unsigned int flags);
+
+size_t
+sd_autolink__url(size_t *rewind_p, struct sd_buf *link,
+	uint8_t *data, size_t offset, size_t size, unsigned int flags);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+/* vim: set filetype=c: */
diff --git a/src/buffer.c b/src/buffer.c
new file mode 100644
index 0000000..519b231
--- /dev/null
+++ b/src/buffer.c
@@ -0,0 +1,225 @@
+/*
+ * Copyright (c) 2008, Natacha Porté
+ * Copyright (c) 2011, Vicent Martí
+ *
+ * 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.
+ */
+
+#define BUFFER_MAX_ALLOC_SIZE (1024 * 1024 * 16) //16mb
+
+#include "buffer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+/* MSVC compat */
+#if defined(_MSC_VER)
+#	define _sd_buf_vsnprintf _vsnprintf
+#else
+#	define _sd_buf_vsnprintf vsnprintf
+#endif
+
+int
+sd_bufprefix(const struct sd_buf *sd_buf, const char *prefix)
+{
+	size_t i;
+	assert(sd_buf && sd_buf->unit);
+
+	for (i = 0; i < sd_buf->size; ++i) {
+		if (prefix[i] == 0)
+			return 0;
+
+		if (sd_buf->data[i] != prefix[i])
+			return sd_buf->data[i] - prefix[i];
+	}
+
+	return 0;
+}
+
+/* sd_bufgrow: increasing the allocated size to the given value */
+int
+sd_bufgrow(struct sd_buf *sd_buf, size_t neosz)
+{
+	size_t neoasz;
+	void *neodata;
+
+	assert(sd_buf && sd_buf->unit);
+
+	if (neosz > BUFFER_MAX_ALLOC_SIZE)
+		return BUF_ENOMEM;
+
+	if (sd_buf->asize >= neosz)
+		return BUF_OK;
+
+	neoasz = sd_buf->asize + sd_buf->unit;
+	while (neoasz < neosz)
+		neoasz += sd_buf->unit;
+
+	neodata = realloc(sd_buf->data, neoasz);
+	if (!neodata)
+		return BUF_ENOMEM;
+
+	sd_buf->data = neodata;
+	sd_buf->asize = neoasz;
+	return BUF_OK;
+}
+
+
+/* sd_bufnew: allocation of a new buffer */
+struct sd_buf *
+sd_bufnew(size_t unit)
+{
+	struct sd_buf *ret;
+	ret = malloc(sizeof (struct sd_buf));
+
+	if (ret) {
+		ret->data = 0;
+		ret->size = ret->asize = 0;
+		ret->unit = unit;
+	}
+	return ret;
+}
+
+/* sd_bufnullterm: NULL-termination of the string array */
+const char *
+sd_bufcstr(struct sd_buf *sd_buf)
+{
+	assert(sd_buf && sd_buf->unit);
+
+	if (sd_buf->size < sd_buf->asize && sd_buf->data[sd_buf->size] == 0)
+		return (char *)sd_buf->data;
+
+	if (sd_buf->size + 1 <= sd_buf->asize || sd_bufgrow(sd_buf, sd_buf->size + 1) == 0) {
+		sd_buf->data[sd_buf->size] = 0;
+		return (char *)sd_buf->data;
+	}
+
+	return NULL;
+}
+
+/* sd_bufprintf: formatted printing to a buffer */
+void
+sd_bufprintf(struct sd_buf *sd_buf, const char *fmt, ...)
+{
+	va_list ap;
+	int n;
+
+	assert(sd_buf && sd_buf->unit);
+
+	if (sd_buf->size >= sd_buf->asize && sd_bufgrow(sd_buf, sd_buf->size + 1) < 0)
+		return;
+	
+	va_start(ap, fmt);
+	n = _sd_buf_vsnprintf((char *)sd_buf->data + sd_buf->size, sd_buf->asize - sd_buf->size, fmt, ap);
+	va_end(ap);
+
+	if (n < 0) {
+#ifdef _MSC_VER
+		va_start(ap, fmt);
+		n = _vscprintf(fmt, ap);
+		va_end(ap);
+#else
+		return;
+#endif
+	}
+
+	if ((size_t)n >= sd_buf->asize - sd_buf->size) {
+		if (sd_bufgrow(sd_buf, sd_buf->size + n + 1) < 0)
+			return;
+
+		va_start(ap, fmt);
+		n = _sd_buf_vsnprintf((char *)sd_buf->data + sd_buf->size, sd_buf->asize - sd_buf->size, fmt, ap);
+		va_end(ap);
+	}
+
+	if (n < 0)
+		return;
+
+	sd_buf->size += n;
+}
+
+/* sd_bufput: appends raw data to a buffer */
+void
+sd_bufput(struct sd_buf *sd_buf, const void *data, size_t len)
+{
+	assert(sd_buf && sd_buf->unit);
+
+	if (sd_buf->size + len > sd_buf->asize && sd_bufgrow(sd_buf, sd_buf->size + len) < 0)
+		return;
+
+	memcpy(sd_buf->data + sd_buf->size, data, len);
+	sd_buf->size += len;
+}
+
+/* sd_bufputs: appends a NUL-terminated string to a buffer */
+void
+sd_bufputs(struct sd_buf *sd_buf, const char *str)
+{
+	sd_bufput(sd_buf, str, strlen(str));
+}
+
+
+/* sd_bufputc: appends a single uint8_t to a buffer */
+void
+sd_bufputc(struct sd_buf *sd_buf, int c)
+{
+	assert(sd_buf && sd_buf->unit);
+
+	if (sd_buf->size + 1 > sd_buf->asize && sd_bufgrow(sd_buf, sd_buf->size + 1) < 0)
+		return;
+
+	sd_buf->data[sd_buf->size] = c;
+	sd_buf->size += 1;
+}
+
+/* sd_bufrelease: decrease the reference count and free the buffer if needed */
+void
+sd_bufrelease(struct sd_buf *sd_buf)
+{
+	if (!sd_buf)
+		return;
+
+	free(sd_buf->data);
+	free(sd_buf);
+}
+
+
+/* sd_bufreset: frees internal data of the buffer */
+void
+sd_bufreset(struct sd_buf *sd_buf)
+{
+	if (!sd_buf)
+		return;
+
+	free(sd_buf->data);
+	sd_buf->data = NULL;
+	sd_buf->size = sd_buf->asize = 0;
+}
+
+/* sd_bufslurp: removes a given number of bytes from the head of the array */
+void
+sd_bufslurp(struct sd_buf *sd_buf, size_t len)
+{
+	assert(sd_buf && sd_buf->unit);
+
+	if (len >= sd_buf->size) {
+		sd_buf->size = 0;
+		return;
+	}
+
+	sd_buf->size -= len;
+	memmove(sd_buf->data, sd_buf->data + len, sd_buf->size);
+}
+
diff --git a/src/buffer.h b/src/buffer.h
new file mode 100644
index 0000000..3b45600
--- /dev/null
+++ b/src/buffer.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2008, Natacha Porté
+ * Copyright (c) 2011, Vicent Martí
+ *
+ * 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 BUFFER_H__
+#define BUFFER_H__
+
+#include <stddef.h>
+#include <stdarg.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(_MSC_VER)
+#define __attribute__(x)
+#define inline
+#endif
+
+typedef enum {
+	BUF_OK = 0,
+	BUF_ENOMEM = -1,
+} sd_buferror_t;
+
+/* struct sd_buf: character array buffer */
+struct sd_buf {
+	uint8_t *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) */
+};
+
+/* CONST_BUF: global buffer from a string litteral */
+#define BUF_STATIC(string) \
+	{ (uint8_t *)string, sizeof string -1, sizeof string, 0, 0 }
+
+/* VOLATILE_BUF: macro for creating a volatile buffer on the stack */
+#define BUF_VOLATILE(strname) \
+	{ (uint8_t *)strname, strlen(strname), 0, 0, 0 }
+
+/* BUFPUTSL: optimized bufputs of a string litteral */
+#define BUFPUTSL(output, literal) \
+	sd_bufput(output, literal, sizeof literal - 1)
+
+/* bufgrow: increasing the allocated size to the given value */
+int sd_bufgrow(struct sd_buf *, size_t);
+
+/* bufnew: allocation of a new buffer */
+struct sd_buf *sd_bufnew(size_t) __attribute__ ((malloc));
+
+/* bufnullterm: NUL-termination of the string array (making a C-string) */
+const char *sd_bufcstr(struct sd_buf *);
+
+/* bufprefix: compare the beginning of a buffer with a string */
+int sd_bufprefix(const struct sd_buf *buf, const char *prefix);
+
+/* bufput: appends raw data to a buffer */
+void sd_bufput(struct sd_buf *, const void *, size_t);
+
+/* bufputs: appends a NUL-terminated string to a buffer */
+void sd_bufputs(struct sd_buf *, const char *);
+
+/* bufputc: appends a single char to a buffer */
+void sd_bufputc(struct sd_buf *, int);
+
+/* bufrelease: decrease the reference count and free the buffer if needed */
+void sd_bufrelease(struct sd_buf *);
+
+/* bufreset: frees internal data of the buffer */
+void sd_bufreset(struct sd_buf *);
+
+/* bufslurp: removes a given number of bytes from the head of the array */
+void sd_bufslurp(struct sd_buf *, size_t);
+
+/* bufprintf: formatted printing to a buffer */
+void sd_bufprintf(struct sd_buf *, const char *, ...) __attribute__ ((format (printf, 2, 3)));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/html_blocks.h b/src/html_blocks.h
new file mode 100644
index 0000000..dcd2331
--- /dev/null
+++ b/src/html_blocks.h
@@ -0,0 +1,206 @@
+/* C code produced by gperf version 3.0.3 */
+/* Command-line: gperf -N find_block_tag -H hash_block_tag -C -c -E --ignore-case html_block_names.txt  */
+/* Computed positions: -k'1-2' */
+
+#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \
+      && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \
+      && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \
+      && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \
+      && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \
+      && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \
+      && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \
+      && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \
+      && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \
+      && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \
+      && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \
+      && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \
+      && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \
+      && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \
+      && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \
+      && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \
+      && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \
+      && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \
+      && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \
+      && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \
+      && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \
+      && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \
+      && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126))
+/* The character set is not based on ISO-646.  */
+error "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gnu-gperf at gnu.org>."
+#endif
+
+/* maximum key range = 37, duplicates = 0 */
+
+#ifndef GPERF_DOWNCASE
+#define GPERF_DOWNCASE 1
+static unsigned char gperf_downcase[256] =
+  {
+      0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,
+     15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,
+     30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,  42,  43,  44,
+     45,  46,  47,  48,  49,  50,  51,  52,  53,  54,  55,  56,  57,  58,  59,
+     60,  61,  62,  63,  64,  97,  98,  99, 100, 101, 102, 103, 104, 105, 106,
+    107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
+    122,  91,  92,  93,  94,  95,  96,  97,  98,  99, 100, 101, 102, 103, 104,
+    105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
+    120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+    135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
+    150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
+    165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
+    180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194,
+    195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
+    210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
+    225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+    240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
+    255
+  };
+#endif
+
+#ifndef GPERF_CASE_STRNCMP
+#define GPERF_CASE_STRNCMP 1
+static int
+gperf_case_strncmp (s1, s2, n)
+     register const char *s1;
+     register const char *s2;
+     register unsigned int n;
+{
+  for (; n > 0;)
+    {
+      unsigned char c1 = gperf_downcase[(unsigned char)*s1++];
+      unsigned char c2 = gperf_downcase[(unsigned char)*s2++];
+      if (c1 != 0 && c1 == c2)
+        {
+          n--;
+          continue;
+        }
+      return (int)c1 - (int)c2;
+    }
+  return 0;
+}
+#endif
+
+#ifdef __GNUC__
+__inline
+#else
+#ifdef __cplusplus
+inline
+#endif
+#endif
+static unsigned int
+sd_hash_block_tag (str, len)
+     register const char *str;
+     register unsigned int len;
+{
+  static const unsigned char asso_values[] =
+    {
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+       8, 30, 25, 20, 15, 10, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38,  0, 38,  0, 38,
+       5,  5,  5, 15,  0, 38, 38,  0, 15, 10,
+       0, 38, 38, 15,  0,  5, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38,  0, 38,
+       0, 38,  5,  5,  5, 15,  0, 38, 38,  0,
+      15, 10,  0, 38, 38, 15,  0,  5, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
+      38, 38, 38, 38, 38, 38, 38
+    };
+  register int hval = len;
+
+  switch (hval)
+    {
+      default:
+        hval += asso_values[(unsigned char)str[1]+1];
+      /*FALLTHROUGH*/
+      case 1:
+        hval += asso_values[(unsigned char)str[0]];
+        break;
+    }
+  return hval;
+}
+
+#ifdef __GNUC__
+__inline
+#ifdef __GNUC_STDC_INLINE__
+__attribute__ ((__gnu_inline__))
+#endif
+#endif
+const char *
+sd_find_block_tag (str, len)
+     register const char *str;
+     register unsigned int len;
+{
+  enum
+    {
+      TOTAL_KEYWORDS = 24,
+      MIN_WORD_LENGTH = 1,
+      MAX_WORD_LENGTH = 10,
+      MIN_HASH_VALUE = 1,
+      MAX_HASH_VALUE = 37
+    };
+
+  static const char * const wordlist[] =
+    {
+      "",
+      "p",
+      "dl",
+      "div",
+      "math",
+      "table",
+      "",
+      "ul",
+      "del",
+      "form",
+      "blockquote",
+      "figure",
+      "ol",
+      "fieldset",
+      "",
+      "h1",
+      "",
+      "h6",
+      "pre",
+      "", "",
+      "script",
+      "h5",
+      "noscript",
+      "",
+      "style",
+      "iframe",
+      "h4",
+      "ins",
+      "", "", "",
+      "h3",
+      "", "", "", "",
+      "h2"
+    };
+
+  if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH)
+    {
+      register int key = sd_hash_block_tag (str, len);
+
+      if (key <= MAX_HASH_VALUE && key >= 0)
+        {
+          register const char *s = wordlist[key];
+
+          if ((((unsigned char)*str ^ (unsigned char)*s) & ~32) == 0 && !gperf_case_strncmp (str, s, len) && s[len] == '\0')
+            return s;
+        }
+    }
+  return 0;
+}
diff --git a/src/markdown.c b/src/markdown.c
new file mode 100644
index 0000000..0a65d08
--- /dev/null
+++ b/src/markdown.c
@@ -0,0 +1,2561 @@
+/* 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 "stack.h"
+
+#include <assert.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdio.h>
+
+#if defined(_WIN32)
+#define strncasecmp	_strnicmp
+#endif
+
+#define REF_TABLE_SIZE 8
+
+#define BUFFER_BLOCK 0
+#define BUFFER_SPAN 1
+
+#define MKD_LI_END 8	/* internal list flag */
+
+#define gperf_case_strncmp(s1, s2, n) strncasecmp(s1, s2, n)
+#define GPERF_DOWNCASE 1
+#define GPERF_CASE_STRNCMP 1
+#include "html_blocks.h"
+
+/***************
+ * LOCAL TYPES *
+ ***************/
+
+/* link_ref: reference to a link */
+struct link_ref {
+	unsigned int id;
+
+	struct sd_buf *link;
+	struct sd_buf *title;
+
+	struct link_ref *next;
+};
+
+/* 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 sd_markdown;
+typedef size_t
+(*char_trigger)(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+
+static size_t char_emphasis(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_linebreak(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_codespan(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_escape(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_entity(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_langle_tag(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_autolink_url(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_autolink_email(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_autolink_www(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_link(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size);
+static size_t char_superscript(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *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 sd_markdown {
+	struct sd_callbacks	cb;
+	void *opaque;
+
+	struct link_ref *refs[REF_TABLE_SIZE];
+	uint8_t active_char[256];
+	struct sd_stack work_sd_bufs[2];
+	unsigned int ext_flags;
+	size_t max_nesting;
+	int in_link_body;
+};
+
+/***************************
+ * HELPER FUNCTIONS *
+ ***************************/
+
+static inline struct sd_buf *
+rndr_newsd_buf(struct sd_markdown *rndr, int type)
+{
+	static const size_t sd_buf_size[2] = {256, 64};
+	struct sd_buf *work = NULL;
+	struct sd_stack *pool = &rndr->work_sd_bufs[type];
+
+	if (pool->size < pool->asize &&
+		pool->item[pool->size] != NULL) {
+		work = pool->item[pool->size++];
+		work->size = 0;
+	} else {
+		work = sd_bufnew(sd_buf_size[type]);
+		sd_stack_push(pool, work);
+	}
+
+	return work;
+}
+
+static inline void
+rndr_popsd_buf(struct sd_markdown *rndr, int type)
+{
+	rndr->work_sd_bufs[type].size--;
+}
+
+static void
+unscape_text(struct sd_buf *ob, struct sd_buf *src)
+{
+	size_t i = 0, org;
+	while (i < src->size) {
+		org = i;
+		while (i < src->size && src->data[i] != '\\')
+			i++;
+
+		if (i > org)
+			sd_bufput(ob, src->data + org, i - org);
+
+		if (i + 1 >= src->size)
+			break;
+
+		sd_bufputc(ob, src->data[i + 1]);
+		i += 2;
+	}
+}
+
+static unsigned int
+hash_link_ref(const uint8_t *link_ref, size_t length)
+{
+	size_t i;
+	unsigned int hash = 0;
+
+	for (i = 0; i < length; ++i)
+		hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash;
+
+	return hash;
+}
+
+static struct link_ref *
+add_link_ref(
+	struct link_ref **references,
+	const uint8_t *name, size_t name_size)
+{
+	struct link_ref *ref = calloc(1, sizeof(struct link_ref));
+
+	if (!ref)
+		return NULL;
+
+	ref->id = hash_link_ref(name, name_size);
+	ref->next = references[ref->id % REF_TABLE_SIZE];
+
+	references[ref->id % REF_TABLE_SIZE] = ref;
+	return ref;
+}
+
+static struct link_ref *
+find_link_ref(struct link_ref **references, uint8_t *name, size_t length)
+{
+	unsigned int hash = hash_link_ref(name, length);
+	struct link_ref *ref = NULL;
+
+	ref = references[hash % REF_TABLE_SIZE];
+
+	while (ref != NULL) {
+		if (ref->id == hash)
+			return ref;
+
+		ref = ref->next;
+	}
+
+	return NULL;
+}
+
+static void
+free_link_refs(struct link_ref **references)
+{
+	size_t i;
+
+	for (i = 0; i < REF_TABLE_SIZE; ++i) {
+		struct link_ref *r = references[i];
+		struct link_ref *next;
+
+		while (r) {
+			next = r->next;
+			sd_bufrelease(r->link);
+			sd_bufrelease(r->title);
+			free(r);
+			r = next;
+		}
+	}
+}
+
+/*
+ * Check whether a char is a Markdown space.
+
+ * Right now we only consider spaces the actual
+ * space and a newline: tabs and carriage returns
+ * are filtered out during the preprocessing phase.
+ *
+ * If we wanted to actually be UTF-8 compliant, we
+ * should instead extract an Unicode codepoint from
+ * this character and check for space properties.
+ */
+static inline int
+_isspace(int c)
+{
+	return c == ' ' || c == '\n';
+}
+
+/****************************
+ * 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(uint8_t *data, size_t size)
+{
+	size_t i = 0, nb = 0;
+
+	/* address is assumed to be: [- at ._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(uint8_t *data, size_t size, enum sd_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] == '"' || data[i] == ' ' || data[i] == '\n')
+					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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t i = 0, end = 0;
+	uint8_t action = 0;
+	struct sd_buf work = { 0, 0, 0, 0 };
+
+	if (rndr->work_sd_bufs[BUFFER_SPAN].size +
+		rndr->work_sd_bufs[BUFFER_BLOCK].size > rndr->max_nesting)
+		return;
+
+	while (i < size) {
+		/* copying inactive chars into the output */
+		while (end < size && (action = rndr->active_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
+			sd_bufput(ob, data + i, end - i);
+
+		if (end >= size) break;
+		i = end;
+
+		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 uint8_t, skipping other constructs */
+static size_t
+find_emph_char(uint8_t *data, size_t size, uint8_t 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;
+		}
+		/* skipping a link */
+		else if (data[i] == '[') {
+			size_t tmp_i = 0;
+			uint8_t 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] == '\n'))
+				i++;
+
+			if (i >= size)
+				return tmp_i;
+
+			switch (data[i]) {
+			case '[':
+				cc = ']'; break;
+
+			case '(':
+				cc = ')'; break;
+
+			default:
+				if (tmp_i)
+					return tmp_i;
+				else
+					continue;
+			}
+
+			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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, uint8_t c)
+{
+	size_t i = 0, len;
+	struct sd_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 (data[i] == c && !_isspace(data[i - 1])) {
+
+			if (rndr->ext_flags & MKDEXT_NO_INTRA_EMPHASIS) {
+				if (i + 1 < size && isalnum(data[i + 1]))
+					continue;
+			}
+
+			work = rndr_newsd_buf(rndr, BUFFER_SPAN);
+			parse_inline(work, rndr, data, i);
+			r = rndr->cb.emphasis(ob, work, rndr->opaque);
+			rndr_popsd_buf(rndr, BUFFER_SPAN);
+			return r ? i + 1 : 0;
+		}
+	}
+
+	return 0;
+}
+
+/* parse_emph2 • parsing single emphase */
+static size_t
+parse_emph2(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, uint8_t c)
+{
+	int (*render_method)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	size_t i = 0, len;
+	struct sd_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_newsd_buf(rndr, BUFFER_SPAN);
+			parse_inline(work, rndr, data, i);
+			r = render_method(ob, work, rndr->opaque);
+			rndr_popsd_buf(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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, uint8_t 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 sd_buf *work = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+			parse_inline(work, rndr, data, i);
+			r = rndr->cb.triple_emphasis(ob, work, rndr->opaque);
+			rndr_popsd_buf(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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	uint8_t c = data[0];
+	size_t ret;
+
+	if (rndr->ext_flags & MKDEXT_NO_INTRA_EMPHASIS) {
+		if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>')
+			return 0;
+	}
+
+	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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *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] == ' ')
+		f_begin++;
+
+	f_end = end - nb;
+	while (f_end > nb && data[f_end-1] == ' ')
+		f_end--;
+
+	/* real code span */
+	if (f_begin < f_end) {
+		struct sd_buf work = { data + f_begin, f_end - f_begin, 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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~";
+	struct sd_buf work = { 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 sd_bufputc(ob, data[1]);
+	} else if (size == 1) {
+		sd_bufputc(ob, data[0]);
+	}
+
+	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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	size_t end = 1;
+	struct sd_buf work = { 0, 0, 0, 0 };
+
+	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 sd_bufput(ob, data, end);
+
+	return end;
+}
+
+/* char_langle_tag • '<' when tags or autolinks are allowed */
+static size_t
+char_langle_tag(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	enum sd_mkd_autolink altype = MKDA_NOT_AUTOLINK;
+	size_t end = tag_length(data, size, &altype);
+	struct sd_buf work = { data, end, 0, 0 };
+	int ret = 0;
+
+	if (end > 2) {
+		if (rndr->cb.autolink && altype != MKDA_NOT_AUTOLINK) {
+			struct sd_buf *u_link = rndr_newsd_buf(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_popsd_buf(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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	struct sd_buf *link, *link_url, *link_text;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.link || rndr->in_link_body)
+		return 0;
+
+	link = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__www(&rewind, link, data, offset, size, 0)) > 0) {
+		link_url = rndr_newsd_buf(rndr, BUFFER_SPAN);
+		BUFPUTSL(link_url, "http://");
+		sd_bufput(link_url, link->data, link->size);
+
+		ob->size -= rewind;
+		if (rndr->cb.normal_text) {
+			link_text = rndr_newsd_buf(rndr, BUFFER_SPAN);
+			rndr->cb.normal_text(link_text, link, rndr->opaque);
+			rndr->cb.link(ob, link_url, NULL, link_text, rndr->opaque);
+			rndr_popsd_buf(rndr, BUFFER_SPAN);
+		} else {
+			rndr->cb.link(ob, link_url, NULL, link, rndr->opaque);
+		}
+		rndr_popsd_buf(rndr, BUFFER_SPAN);
+	}
+
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+static size_t
+char_autolink_email(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	struct sd_buf *link;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.autolink || rndr->in_link_body)
+		return 0;
+
+	link = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__email(&rewind, link, data, offset, size, 0)) > 0) {
+		ob->size -= rewind;
+		rndr->cb.autolink(ob, link, MKDA_EMAIL, rndr->opaque);
+	}
+
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+static size_t
+char_autolink_url(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	struct sd_buf *link;
+	size_t link_len, rewind;
+
+	if (!rndr->cb.autolink || rndr->in_link_body)
+		return 0;
+
+	link = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+	if ((link_len = sd_autolink__url(&rewind, link, data, offset, size, 0)) > 0) {
+		ob->size -= rewind;
+		rndr->cb.autolink(ob, link, MKDA_NORMAL, rndr->opaque);
+	}
+
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+	return link_len;
+}
+
+/* char_link • '[': parsing a link or an image */
+static size_t
+char_link(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *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 sd_buf *content = 0;
+	struct sd_buf *link = 0;
+	struct sd_buf *title = 0;
+	struct sd_buf *u_link = 0;
+	size_t org_work_size = rndr->work_sd_bufs[BUFFER_SPAN].size;
+	int text_has_nl = 0, ret = 0;
+	int in_title = 0, qtype = 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] == ')') break;
+			else if (i >= 1 && _isspace(data[i-1]) && (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] == '"') {
+			qtype = data[i];
+			in_title = 1;
+			i++;
+			title_b = i;
+
+			while (i < size) {
+				if (data[i] == '\\') i += 2;
+				else if (data[i] == qtype) {in_title = 0; i++;}
+				else if ((data[i] == ')') && !in_title) 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_newsd_buf(rndr, BUFFER_SPAN);
+			sd_bufput(link, data + link_b, link_e - link_b);
+		}
+
+		if (title_e > title_b) {
+			title = rndr_newsd_buf(rndr, BUFFER_SPAN);
+			sd_bufput(title, data + title_b, title_e - title_b);
+		}
+
+		i++;
+	}
+
+	/* reference style link */
+	else if (i < size && data[i] == '[') {
+		struct sd_buf id = { 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 sd_buf *b = rndr_newsd_buf(rndr, BUFFER_SPAN);
+				size_t j;
+
+				for (j = 1; j < txt_e; j++) {
+					if (data[j] != '\n')
+						sd_bufputc(b, data[j]);
+					else if (data[j - 1] != ' ')
+						sd_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 = find_link_ref(rndr->refs, id.data, id.size);
+		if (!lr)
+			goto cleanup;
+
+		/* keeping link and title from link_ref */
+		link = lr->link;
+		title = lr->title;
+		i++;
+	}
+
+	/* shortcut reference style link */
+	else {
+		struct sd_buf id = { 0, 0, 0, 0 };
+		struct link_ref *lr;
+
+		/* crafting the id */
+		if (text_has_nl) {
+			struct sd_buf *b = rndr_newsd_buf(rndr, BUFFER_SPAN);
+			size_t j;
+
+			for (j = 1; j < txt_e; j++) {
+				if (data[j] != '\n')
+					sd_bufputc(b, data[j]);
+				else if (data[j - 1] != ' ')
+					sd_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 = find_link_ref(rndr->refs, id.data, id.size);
+		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_newsd_buf(rndr, BUFFER_SPAN);
+		if (is_img) {
+			sd_bufput(content, data + 1, txt_e - 1);
+		} else {
+			/* disable autolinking when parsing inline the
+			 * content of a link */
+			rndr->in_link_body = 1;
+			parse_inline(content, rndr, data + 1, txt_e - 1);
+			rndr->in_link_body = 0;
+		}
+	}
+
+	if (link) {
+		u_link = rndr_newsd_buf(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_sd_bufs[BUFFER_SPAN].size = (int)org_work_size;
+	return ret ? i : 0;
+}
+
+static size_t
+char_superscript(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset, size_t size)
+{
+	size_t sup_start, sup_len;
+	struct sd_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_newsd_buf(rndr, BUFFER_SPAN);
+	parse_inline(sup, rndr, data + sup_start, sup_len - sup_start);
+	rndr->cb.superscript(ob, sup, rndr->opaque);
+	rndr_popsd_buf(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(uint8_t *data, size_t size)
+{
+	size_t i;
+
+	for (i = 0; i < size && data[i] != '\n'; i++)
+		if (data[i] != ' ')
+			return 0;
+
+	return i + 1;
+}
+
+/* is_hrule • returns whether a line is a horizontal rule */
+static int
+is_hrule(uint8_t *data, size_t size)
+{
+	size_t i = 0, n = 0;
+	uint8_t 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 uint8_t */
+	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] != ' ')
+			return 0;
+
+		i++;
+	}
+
+	return n >= 3;
+}
+
+/* check if a line begins with a code fence; return the
+ * width of the code fence */
+static size_t
+prefix_codefence(uint8_t *data, size_t size)
+{
+	size_t i = 0, n = 0;
+	uint8_t 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 uint8_t */
+	if (i + 2 >= size || !(data[i] == '~' || data[i] == '`'))
+		return 0;
+
+	c = data[i];
+
+	/* the whole line must be the uint8_t or whitespace */
+	while (i < size && data[i] == c) {
+		n++; i++;
+	}
+
+	if (n < 3)
+		return 0;
+
+	return i;
+}
+
+/* check if a line is a code fence; return its size if it is */
+static size_t
+is_codefence(uint8_t *data, size_t size, struct sd_buf *syntax)
+{
+	size_t i = 0, syn_len = 0;
+	uint8_t *syn_start;
+
+	i = prefix_codefence(data, size);
+	if (i == 0)
+		return 0;
+
+	while (i < size && data[i] == ' ')
+		i++;
+
+	syn_start = data + i;
+
+	if (i < size && data[i] == '{') {
+		i++; syn_start++;
+
+		while (i < size && data[i] != '}' && data[i] != '\n') {
+			syn_len++; i++;
+		}
+
+		if (i == size || data[i] != '}')
+			return 0;
+
+		/* strip all whitespace at the beginning and the end
+		 * of the {} block */
+		while (syn_len > 0 && _isspace(syn_start[0])) {
+			syn_start++; syn_len--;
+		}
+
+		while (syn_len > 0 && _isspace(syn_start[syn_len - 1]))
+			syn_len--;
+
+		i++;
+	} else {
+		while (i < size && !_isspace(data[i])) {
+			syn_len++; i++;
+		}
+	}
+
+	if (syntax) {
+		syntax->data = syn_start;
+		syntax->size = syn_len;
+	}
+
+	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 sd_markdown *rndr, uint8_t *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] != ' ')
+			return 0;
+	}
+
+	return 1;
+}
+
+/* is_headerline • returns whether the line is a setext-style hdr underline */
+static int
+is_headerline(uint8_t *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] == ' ') 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] == ' ') i++;
+		return (i >= size || data[i] == '\n') ? 2 : 0; }
+
+	return 0;
+}
+
+static int
+is_next_headerline(uint8_t *data, size_t size)
+{
+	size_t i = 0;
+
+	while (i < size && data[i] != '\n')
+		i++;
+
+	if (++i >= size)
+		return 0;
+
+	return is_headerline(data + i, size - i);
+}
+
+/* prefix_quote • returns blockquote prefix length */
+static size_t
+prefix_quote(uint8_t *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] == ' ')
+			return i + 2;
+
+		return i + 1;
+	}
+
+	return 0;
+}
+
+/* prefix_code • returns prefix length for block code*/
+static size_t
+prefix_code(uint8_t *data, size_t size)
+{
+	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(uint8_t *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] != ' ')
+		return 0;
+
+	if (is_next_headerline(data + i, size - i))
+		return 0;
+
+	return i + 2;
+}
+
+/* prefix_uli • returns ordered list item prefix */
+static size_t
+prefix_uli(uint8_t *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] != ' ')
+		return 0;
+
+	if (is_next_headerline(data + i, size - i))
+		return 0;
+
+	return i + 2;
+}
+
+
+/* parse_block • parsing of one block, returning next uint8_t to parse */
+static void parse_block(struct sd_buf *ob, struct sd_markdown *rndr,
+			uint8_t *data, size_t size);
+
+
+/* parse_blockquote • handles parsing of a blockquote fragment */
+static size_t
+parse_blockquote(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t beg, end = 0, pre, work_size = 0;
+	uint8_t *work_data = 0;
+	struct sd_buf *out = 0;
+
+	out = rndr_newsd_buf(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 */
+			/* sd_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_popsd_buf(rndr, BUFFER_BLOCK);
+	return end;
+}
+
+static size_t
+parse_htmlblock(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, int do_render);
+
+/* parse_blockquote • handles parsing of a regular paragraph */
+static size_t
+parse_paragraph(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t i = 0, end = 0;
+	int level = 0;
+	struct sd_buf work = { data, 0, 0, 0 };
+
+	while (i < size) {
+		for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */;
+
+		if (is_empty(data + i, size - i))
+			break;
+
+		if ((level = is_headerline(data + i, size - i)) != 0)
+			break;
+
+		if (is_atxheader(rndr, data + i, size - i) ||
+			is_hrule(data + i, size - i) ||
+			prefix_quote(data + i, size - i)) {
+			end = i;
+			break;
+		}
+
+		/*
+		 * Early termination of a paragraph with the same logic
+		 * as Markdown 1.0.0. If this logic is applied, the
+		 * Markdown 1.0.3 test suite won't pass cleanly
+		 *
+		 * :: If the first character in a new line is not a letter,
+		 * let's check to see if there's some kind of block starting
+		 * here
+		 */
+		if ((rndr->ext_flags & MKDEXT_LAX_SPACING) && !isalnum(data[i])) {
+			if (prefix_oli(data + i, size - i) ||
+				prefix_uli(data + i, size - i)) {
+				end = i;
+				break;
+			}
+
+			/* see if an html block starts here */
+			if (data[i] == '<' && rndr->cb.blockhtml &&
+				parse_htmlblock(ob, rndr, data + i, size - i, 0)) {
+				end = i;
+				break;
+			}
+
+			/* see if a code fence starts here */
+			if ((rndr->ext_flags & MKDEXT_FENCED_CODE) != 0 &&
+				is_codefence(data + i, size - i, NULL) != 0) {
+				end = i;
+				break;
+			}
+		}
+
+		i = end;
+	}
+
+	work.size = i;
+	while (work.size && data[work.size - 1] == '\n')
+		work.size--;
+
+	if (!level) {
+		struct sd_buf *tmp = rndr_newsd_buf(rndr, BUFFER_BLOCK);
+		parse_inline(tmp, rndr, work.data, work.size);
+		if (rndr->cb.paragraph)
+			rndr->cb.paragraph(ob, tmp, rndr->opaque);
+		rndr_popsd_buf(rndr, BUFFER_BLOCK);
+	} else {
+		struct sd_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 sd_buf *tmp = rndr_newsd_buf(rndr, BUFFER_BLOCK);
+				parse_inline(tmp, rndr, work.data, work.size);
+
+				if (rndr->cb.paragraph)
+					rndr->cb.paragraph(ob, tmp, rndr->opaque);
+
+				rndr_popsd_buf(rndr, BUFFER_BLOCK);
+				work.data += beg;
+				work.size = i - beg;
+			}
+			else work.size = i;
+		}
+
+		header_work = rndr_newsd_buf(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_popsd_buf(rndr, BUFFER_SPAN);
+	}
+
+	return end;
+}
+
+/* parse_fencedcode • handles parsing of a block-level code fragment */
+static size_t
+parse_fencedcode(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t beg, end;
+	struct sd_buf *work = 0;
+	struct sd_buf lang = { 0, 0, 0, 0 };
+
+	beg = is_codefence(data, size, &lang);
+	if (beg == 0) return 0;
+
+	work = rndr_newsd_buf(rndr, BUFFER_BLOCK);
+
+	while (beg < size) {
+		size_t fence_end;
+		struct sd_buf fence_trail = { 0, 0, 0, 0 };
+
+		fence_end = is_codefence(data + beg, size - beg, &fence_trail);
+		if (fence_end != 0 && fence_trail.size == 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))
+				sd_bufputc(work, '\n');
+			else sd_bufput(work, data + beg, end - beg);
+		}
+		beg = end;
+	}
+
+	if (work->size && work->data[work->size - 1] != '\n')
+		sd_bufputc(work, '\n');
+
+	if (rndr->cb.blockcode)
+		rndr->cb.blockcode(ob, work, lang.size ? &lang : NULL, rndr->opaque);
+
+	rndr_popsd_buf(rndr, BUFFER_BLOCK);
+	return beg;
+}
+
+static size_t
+parse_blockcode(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t beg, end, pre;
+	struct sd_buf *work = 0;
+
+	work = rndr_newsd_buf(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))
+				sd_bufputc(work, '\n');
+			else sd_bufput(work, data + beg, end - beg);
+		}
+		beg = end;
+	}
+
+	while (work->size && work->data[work->size - 1] == '\n')
+		work->size -= 1;
+
+	sd_bufputc(work, '\n');
+
+	if (rndr->cb.blockcode)
+		rndr->cb.blockcode(ob, work, NULL, rndr->opaque);
+
+	rndr_popsd_buf(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 sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, int *flags)
+{
+	struct sd_buf *work = 0, *inter = 0;
+	size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
+	int in_empty = 0, has_inside_empty = 0, in_fence = 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_newsd_buf(rndr, BUFFER_SPAN);
+	inter = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+	/* putting the first line into the working buffer */
+	sd_bufput(work, data + beg, end - beg);
+	beg = end;
+
+	/* process the following lines */
+	while (beg < size) {
+		size_t has_next_uli = 0, has_next_oli = 0;
+
+		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 (rndr->ext_flags & MKDEXT_FENCED_CODE) {
+			if (is_codefence(data + beg + i, end - beg - i, NULL) != 0)
+				in_fence = !in_fence;
+		}
+
+		/* Only check for new list items if we are **not** inside
+		 * a fenced code block */
+		if (!in_fence) {
+			has_next_uli = prefix_uli(data + beg + i, end - beg - i);
+			has_next_oli = prefix_oli(data + beg + i, end - beg - i);
+		}
+
+		/* checking for ul/ol switch */
+		if (in_empty && (
+			((*flags & MKD_LIST_ORDERED) && has_next_uli) ||
+			(!(*flags & MKD_LIST_ORDERED) && has_next_oli))){
+			*flags |= MKD_LI_END;
+			break; /* the following item must have same list type */
+		}
+
+		/* checking for a new item */
+		if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) {
+			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;
+		 * note that now we only require 1 space of indentation
+		 * to continue a list */
+		else if (in_empty && pre == 0) {
+			*flags |= MKD_LI_END;
+			break;
+		}
+		else if (in_empty) {
+			sd_bufputc(work, '\n');
+			has_inside_empty = 1;
+		}
+
+		in_empty = 0;
+
+		/* adding the line without prefix into the working buffer */
+		sd_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_popsd_buf(rndr, BUFFER_SPAN);
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+	return beg;
+}
+
+
+/* parse_list • parsing ordered or unordered list block */
+static size_t
+parse_list(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, int flags)
+{
+	struct sd_buf *work = 0;
+	size_t i = 0, j;
+
+	work = rndr_newsd_buf(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_popsd_buf(rndr, BUFFER_BLOCK);
+	return i;
+}
+
+/* parse_atxheader • parsing of atx-style headers */
+static size_t
+parse_atxheader(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *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] == ' '; i++);
+
+	for (end = i; end < size && data[end] != '\n'; end++);
+	skip = end;
+
+	while (end && data[end - 1] == '#')
+		end--;
+
+	while (end && data[end - 1] == ' ')
+		end--;
+
+	if (end > i) {
+		struct sd_buf *work = rndr_newsd_buf(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_popsd_buf(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_tag(
+	const char *tag,
+	size_t tag_len,
+	struct sd_markdown *rndr,
+	uint8_t *data,
+	size_t size)
+{
+	size_t i, w;
+
+	/* checking if tag is a match */
+	if (tag_len + 3 >= size ||
+		strncasecmp((char *)data + 2, tag, tag_len) != 0 ||
+		data[tag_len + 2] != '>')
+		return 0;
+
+	/* checking white lines */
+	i = tag_len + 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 (i < size)
+		w = is_empty(data + i, size - i);
+
+	return i + w;
+}
+
+static size_t
+htmlblock_end(const char *curtag,
+	struct sd_markdown *rndr,
+	uint8_t *data,
+	size_t size,
+	int start_of_line)
+{
+	size_t tag_size = strlen(curtag);
+	size_t i = 1, end_tag;
+	int block_lines = 0;
+
+	while (i < size) {
+		i++;
+		while (i < size && !(data[i - 1] == '<' && data[i] == '/')) {
+			if (data[i] == '\n')
+				block_lines++;
+
+			i++;
+		}
+
+		/* If we are only looking for unindented tags, skip the tag
+		 * if it doesn't follow a newline.
+		 *
+		 * The only exception to this is if the tag is still on the
+		 * initial line; in that case it still counts as a closing
+		 * tag
+		 */
+		if (start_of_line && block_lines > 0 && data[i - 2] != '\n')
+			continue;
+
+		if (i + 2 + tag_size >= size)
+			break;
+
+		end_tag = htmlblock_end_tag(curtag, tag_size, rndr, data + i - 1, size - i + 1);
+		if (end_tag)
+			return i + end_tag - 1;
+	}
+
+	return 0;
+}
+
+
+/* parse_htmlblock • parsing of inline HTML block */
+static size_t
+parse_htmlblock(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size, int do_render)
+{
+	size_t i, j = 0, tag_end;
+	const char *curtag = NULL;
+	struct sd_buf work = { data, 0, 0, 0 };
+
+	/* identification of the opening tag */
+	if (size < 2 || data[0] != '<')
+		return 0;
+
+	i = 1;
+	while (i < size && data[i] != '>' && data[i] != ' ')
+		i++;
+
+	if (i < size)
+		curtag = sd_find_block_tag((char *)data + 1, (int)i - 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 */
+	tag_end = htmlblock_end(curtag, rndr, data, size, 1);
+
+	/* if not found, trying a second pass looking for indented match */
+	/* but not if tag is "ins" or "del" (following original Markdown.pl) */
+	if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0) {
+		tag_end = htmlblock_end(curtag, rndr, data, size, 0);
+	}
+
+	if (!tag_end)
+		return 0;
+
+	/* the end of the block has been found */
+	work.size = tag_end;
+	if (do_render && rndr->cb.blockhtml)
+		rndr->cb.blockhtml(ob, &work, rndr->opaque);
+
+	return tag_end;
+}
+
+static void
+parse_table_row(
+	struct sd_buf *ob,
+	struct sd_markdown *rndr,
+	uint8_t *data,
+	size_t size,
+	size_t columns,
+	int *col_data,
+	int header_flag)
+{
+	size_t i = 0, col;
+	struct sd_buf *row_work = 0;
+
+	if (!rndr->cb.table_cell || !rndr->cb.table_row)
+		return;
+
+	row_work = rndr_newsd_buf(rndr, BUFFER_SPAN);
+
+	if (i < size && data[i] == '|')
+		i++;
+
+	for (col = 0; col < columns && i < size; ++col) {
+		size_t cell_start, cell_end;
+		struct sd_buf *cell_work;
+
+		cell_work = rndr_newsd_buf(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] | header_flag, rndr->opaque);
+
+		rndr_popsd_buf(rndr, BUFFER_SPAN);
+		i++;
+	}
+
+	for (; col < columns; ++col) {
+		struct sd_buf empty_cell = { 0, 0, 0, 0 };
+		rndr->cb.table_cell(row_work, &empty_cell, col_data[col] | header_flag, rndr->opaque);
+	}
+
+	rndr->cb.table_row(ob, row_work, rndr->opaque);
+
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+}
+
+static size_t
+parse_table_header(
+	struct sd_buf *ob,
+	struct sd_markdown *rndr,
+	uint8_t *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;
+
+	while (header_end > 0 && _isspace(data[header_end - 1]))
+		header_end--;
+
+	if (data[0] == '|')
+		pipes--;
+
+	if (header_end && data[header_end - 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;
+
+		while (i < under_end && data[i] == ' ')
+			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] == ' ')
+			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,
+		MKD_TABLE_HEADER
+	);
+
+	return under_end + 1;
+}
+
+static size_t
+parse_table(
+	struct sd_buf *ob,
+	struct sd_markdown *rndr,
+	uint8_t *data,
+	size_t size)
+{
+	size_t i;
+
+	struct sd_buf *header_work = 0;
+	struct sd_buf *body_work = 0;
+
+	size_t columns;
+	int *col_data = NULL;
+
+	header_work = rndr_newsd_buf(rndr, BUFFER_SPAN);
+	body_work = rndr_newsd_buf(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, 0
+			);
+
+			i++;
+		}
+
+		if (rndr->cb.table)
+			rndr->cb.table(ob, header_work, body_work, rndr->opaque);
+	}
+
+	free(col_data);
+	rndr_popsd_buf(rndr, BUFFER_SPAN);
+	rndr_popsd_buf(rndr, BUFFER_BLOCK);
+	return i;
+}
+
+/* parse_block • parsing of one block, returning next uint8_t to parse */
+static void
+parse_block(struct sd_buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size)
+{
+	size_t beg, end, i;
+	uint8_t *txt_data;
+	beg = 0;
+
+	if (rndr->work_sd_bufs[BUFFER_SPAN].size +
+		rndr->work_sd_bufs[BUFFER_BLOCK].size > 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(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **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;
+
+	/* 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] == ' ') 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] == ' ') 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] != '\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] == ' ') 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] == ' ') 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] == ' ')
+			i -= 1;
+		if (i > title_offset
+		&& (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
+			line_end = title_end;
+			title_end = i; } }
+
+	if (!line_end || link_end == link_offset)
+		return 0; /* garbage after the link empty link */
+
+	/* a valid ref has been found, filling-in return structures */
+	if (last)
+		*last = line_end;
+
+	if (refs) {
+		struct link_ref *ref;
+
+		ref = add_link_ref(refs, data + id_offset, id_end - id_offset);
+		if (!ref)
+			return 0;
+
+		ref->link = sd_bufnew(link_end - link_offset);
+		sd_bufput(ref->link, data + link_offset, link_end - link_offset);
+
+		if (title_end > title_offset) {
+			ref->title = sd_bufnew(title_end - title_offset);
+			sd_bufput(ref->title, data + title_offset, title_end - title_offset);
+		}
+	}
+
+	return 1;
+}
+
+static void expand_tabs(struct sd_buf *ob, const uint8_t *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)
+			sd_bufput(ob, line + org, i - org);
+
+		if (i >= size)
+			break;
+
+		do {
+			sd_bufputc(ob, ' '); tab++;
+		} while (tab % 4);
+
+		i++;
+	}
+}
+
+/**********************
+ * EXPORTED FUNCTIONS *
+ **********************/
+
+struct sd_markdown *
+sd_markdown_new(
+	unsigned int extensions,
+	size_t max_nesting,
+	const struct sd_callbacks *callbacks,
+	void *opaque)
+{
+	struct sd_markdown *md = NULL;
+
+	assert(max_nesting > 0 && callbacks);
+
+	md = malloc(sizeof(struct sd_markdown));
+	if (!md)
+		return NULL;
+
+	memcpy(&md->cb, callbacks, sizeof(struct sd_callbacks));
+
+	sd_stack_init(&md->work_sd_bufs[BUFFER_BLOCK], 4);
+	sd_stack_init(&md->work_sd_bufs[BUFFER_SPAN], 8);
+
+	memset(md->active_char, 0x0, 256);
+
+	if (md->cb.emphasis || md->cb.double_emphasis || md->cb.triple_emphasis) {
+		md->active_char['*'] = MD_CHAR_EMPHASIS;
+		md->active_char['_'] = MD_CHAR_EMPHASIS;
+		if (extensions & MKDEXT_STRIKETHROUGH)
+			md->active_char['~'] = MD_CHAR_EMPHASIS;
+	}
+
+	if (md->cb.codespan)
+		md->active_char['`'] = MD_CHAR_CODESPAN;
+
+	if (md->cb.linebreak)
+		md->active_char['\n'] = MD_CHAR_LINEBREAK;
+
+	if (md->cb.image || md->cb.link)
+		md->active_char['['] = MD_CHAR_LINK;
+
+	md->active_char['<'] = MD_CHAR_LANGLE;
+	md->active_char['\\'] = MD_CHAR_ESCAPE;
+	md->active_char['&'] = MD_CHAR_ENTITITY;
+
+	if (extensions & MKDEXT_AUTOLINK) {
+		md->active_char[':'] = MD_CHAR_AUTOLINK_URL;
+		md->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
+		md->active_char['w'] = MD_CHAR_AUTOLINK_WWW;
+	}
+
+	if (extensions & MKDEXT_SUPERSCRIPT)
+		md->active_char['^'] = MD_CHAR_SUPERSCRIPT;
+
+	/* Extension data */
+	md->ext_flags = extensions;
+	md->opaque = opaque;
+	md->max_nesting = max_nesting;
+	md->in_link_body = 0;
+
+	return md;
+}
+
+void
+sd_markdown_render(struct sd_buf *ob, const uint8_t *document, size_t doc_size, struct sd_markdown *md)
+{
+#define MARKDOWN_GROW(x) ((x) + ((x) >> 1))
+	static const char UTF8_BOM[] = {0xEF, 0xBB, 0xBF};
+
+	struct sd_buf *text;
+	size_t beg, end;
+
+	text = sd_bufnew(64);
+	if (!text)
+		return;
+
+	/* Preallocate enough space for our buffer to avoid expanding while copying */
+	sd_bufgrow(text, doc_size);
+
+	/* reset the references table */
+	memset(&md->refs, 0x0, REF_TABLE_SIZE * sizeof(void *));
+
+	/* first pass: looking for references, copying everything else */
+	beg = 0;
+
+	/* Skip a possible UTF-8 BOM, even though the Unicode standard
+	 * discourages having these in UTF-8 documents */
+	if (doc_size >= 3 && memcmp(document, UTF8_BOM, 3) == 0)
+		beg += 3;
+
+	while (beg < doc_size) /* iterating over lines */
+		if (is_ref(document, beg, doc_size, &end, md->refs))
+			beg = end;
+		else { /* skipping to the next line */
+			end = beg;
+			while (end < doc_size && document[end] != '\n' && document[end] != '\r')
+				end++;
+
+			/* adding the line body if present */
+			if (end > beg)
+				expand_tabs(text, document + beg, end - beg);
+
+			while (end < doc_size && (document[end] == '\n' || document[end] == '\r')) {
+				/* add one \n per newline */
+				if (document[end] == '\n' || (end + 1 < doc_size && document[end + 1] != '\n'))
+					sd_bufputc(text, '\n');
+				end++;
+			}
+
+			beg = end;
+		}
+
+	/* pre-grow the output buffer to minimize allocations */
+	sd_bufgrow(ob, MARKDOWN_GROW(text->size));
+
+	/* second pass: actual rendering */
+	if (md->cb.doc_header)
+		md->cb.doc_header(ob, md->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')
+			sd_bufputc(text, '\n');
+
+		parse_block(ob, md, text->data, text->size);
+	}
+
+	if (md->cb.doc_footer)
+		md->cb.doc_footer(ob, md->opaque);
+
+	/* clean-up */
+	sd_bufrelease(text);
+	free_link_refs(md->refs);
+
+	assert(md->work_sd_bufs[BUFFER_SPAN].size == 0);
+	assert(md->work_sd_bufs[BUFFER_BLOCK].size == 0);
+}
+
+void
+sd_markdown_free(struct sd_markdown *md)
+{
+	size_t i;
+
+	for (i = 0; i < (size_t)md->work_sd_bufs[BUFFER_SPAN].asize; ++i)
+		sd_bufrelease(md->work_sd_bufs[BUFFER_SPAN].item[i]);
+
+	for (i = 0; i < (size_t)md->work_sd_bufs[BUFFER_BLOCK].asize; ++i)
+		sd_bufrelease(md->work_sd_bufs[BUFFER_BLOCK].item[i]);
+
+	sd_stack_free(&md->work_sd_bufs[BUFFER_SPAN]);
+	sd_stack_free(&md->work_sd_bufs[BUFFER_BLOCK]);
+
+	free(md);
+}
+
+void
+sd_version(int *ver_major, int *ver_minor, int *ver_revision)
+{
+	*ver_major = SUNDOWN_VER_MAJOR;
+	*ver_minor = SUNDOWN_VER_MINOR;
+	*ver_revision = SUNDOWN_VER_REVISION;
+}
+
+/* vim: set filetype=c: */
+
+#if defined(_MSC_VER)
+#include "lua.h" 
+int __declspec(dllexport) __cdecl luaopen_libsundown(lua_State* L) { return 0; } 
+#endif 
diff --git a/src/markdown.h b/src/markdown.h
new file mode 100644
index 0000000..87d0dfc
--- /dev/null
+++ b/src/markdown.h
@@ -0,0 +1,138 @@
+/* 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"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SUNDOWN_VERSION "1.16.0"
+#define SUNDOWN_VER_MAJOR 1
+#define SUNDOWN_VER_MINOR 16
+#define SUNDOWN_VER_REVISION 0
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* mkd_autolink - type of autolink */
+enum sd_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 sd_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 sd_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_SPACE_HEADERS = (1 << 6),
+	MKDEXT_SUPERSCRIPT = (1 << 7),
+	MKDEXT_LAX_SPACING = (1 << 8),
+};
+
+/* sd_callbacks - functions for rendering parsed data */
+struct sd_callbacks {
+	/* block level callbacks - NULL skips the block */
+	void (*blockcode)(struct sd_buf *ob, const struct sd_buf *text, const struct sd_buf *lang, void *opaque);
+	void (*blockquote)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*blockhtml)(struct sd_buf *ob,const  struct sd_buf *text, void *opaque);
+	void (*header)(struct sd_buf *ob, const struct sd_buf *text, int level, void *opaque);
+	void (*hrule)(struct sd_buf *ob, void *opaque);
+	void (*list)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+	void (*listitem)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+	void (*paragraph)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*table)(struct sd_buf *ob, const struct sd_buf *header, const struct sd_buf *body, void *opaque);
+	void (*table_row)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	void (*table_cell)(struct sd_buf *ob, const struct sd_buf *text, int flags, void *opaque);
+
+
+	/* span level callbacks - NULL or return 0 prints the span verbatim */
+	int (*autolink)(struct sd_buf *ob, const struct sd_buf *link, enum sd_mkd_autolink type, void *opaque);
+	int (*codespan)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*double_emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*image)(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *alt, void *opaque);
+	int (*linebreak)(struct sd_buf *ob, void *opaque);
+	int (*link)(struct sd_buf *ob, const struct sd_buf *link, const struct sd_buf *title, const struct sd_buf *content, void *opaque);
+	int (*raw_html_tag)(struct sd_buf *ob, const struct sd_buf *tag, void *opaque);
+	int (*triple_emphasis)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*strikethrough)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+	int (*superscript)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+
+	/* low level callbacks - NULL copies input directly into the output */
+	void (*entity)(struct sd_buf *ob, const struct sd_buf *entity, void *opaque);
+	void (*normal_text)(struct sd_buf *ob, const struct sd_buf *text, void *opaque);
+
+	/* header and footer */
+	void (*doc_header)(struct sd_buf *ob, void *opaque);
+	void (*doc_footer)(struct sd_buf *ob, void *opaque);
+};
+
+struct sd_markdown;
+
+/*********
+ * FLAGS *
+ *********/
+
+/* list/listitem flags */
+#define MKD_LIST_ORDERED	1
+#define MKD_LI_BLOCK		2  /* <li> containing block data */
+
+/**********************
+ * EXPORTED FUNCTIONS *
+ **********************/
+
+extern struct sd_markdown *
+sd_markdown_new(
+	unsigned int extensions,
+	size_t max_nesting,
+	const struct sd_callbacks *callbacks,
+	void *opaque);
+
+extern void
+sd_markdown_render(struct sd_buf *ob, const uint8_t *document, size_t doc_size, struct sd_markdown *md);
+
+extern void
+sd_markdown_free(struct sd_markdown *md);
+
+extern void
+sd_version(int *major, int *minor, int *revision);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+/* vim: set filetype=c: */
diff --git a/src/stack.c b/src/stack.c
new file mode 100644
index 0000000..442d440
--- /dev/null
+++ b/src/stack.c
@@ -0,0 +1,81 @@
+#include "stack.h"
+#include <string.h>
+
+int
+sd_stack_grow(struct sd_stack *st, size_t new_size)
+{
+	void **new_st;
+
+	if (st->asize >= new_size)
+		return 0;
+
+	new_st = realloc(st->item, new_size * sizeof(void *));
+	if (new_st == NULL)
+		return -1;
+
+	memset(new_st + st->asize, 0x0,
+		(new_size - st->asize) * sizeof(void *));
+
+	st->item = new_st;
+	st->asize = new_size;
+
+	if (st->size > new_size)
+		st->size = new_size;
+
+	return 0;
+}
+
+void
+sd_stack_free(struct sd_stack *st)
+{
+	if (!st)
+		return;
+
+	free(st->item);
+
+	st->item = NULL;
+	st->size = 0;
+	st->asize = 0;
+}
+
+int
+sd_stack_init(struct sd_stack *st, size_t initial_size)
+{
+	st->item = NULL;
+	st->size = 0;
+	st->asize = 0;
+
+	if (!initial_size)
+		initial_size = 8;
+
+	return sd_stack_grow(st, initial_size);
+}
+
+void *
+sd_stack_pop(struct sd_stack *st)
+{
+	if (!st->size)
+		return NULL;
+
+	return st->item[--st->size];
+}
+
+int
+sd_stack_push(struct sd_stack *st, void *item)
+{
+	if (sd_stack_grow(st, st->size * 2) < 0)
+		return -1;
+
+	st->item[st->size++] = item;
+	return 0;
+}
+
+void *
+sd_stack_top(struct sd_stack *st)
+{
+	if (!st->size)
+		return NULL;
+
+	return st->item[st->size - 1];
+}
+
diff --git a/src/stack.h b/src/stack.h
new file mode 100644
index 0000000..4e66d22
--- /dev/null
+++ b/src/stack.h
@@ -0,0 +1,29 @@
+#ifndef STACK_H__
+#define STACK_H__
+
+#include <stdlib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct sd_stack {
+	void **item;
+	size_t size;
+	size_t asize;
+};
+
+void sd_stack_free(struct sd_stack *);
+int sd_stack_grow(struct sd_stack *, size_t);
+int sd_stack_init(struct sd_stack *, size_t);
+
+int sd_stack_push(struct sd_stack *, void *);
+
+void *sd_stack_pop(struct sd_stack *);
+void *sd_stack_top(struct sd_stack *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/lua-torch-sundown.git



More information about the debian-science-commits mailing list