diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## MMark 0.0.1.0
+
+* Initial release.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,28 @@
+Copyright © 2017 Mark Karpov
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* 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.
+
+* Neither the name Mark Karpov nor the names of 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 “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 HOLDERS 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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,229 @@
+# MMark
+
+[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause)
+[![Hackage](https://img.shields.io/hackage/v/mmark.svg?style=flat)](https://hackage.haskell.org/package/mmark)
+[![Stackage Nightly](http://stackage.org/package/mmark/badge/nightly)](http://stackage.org/nightly/package/mmark)
+[![Stackage LTS](http://stackage.org/package/mmark/badge/lts)](http://stackage.org/lts/package/mmark)
+[![Build Status](https://travis-ci.org/mrkkrp/mmark.svg?branch=master)](https://travis-ci.org/mrkkrp/mmark)
+[![Coverage Status](https://coveralls.io/repos/mrkkrp/mmark/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/mmark?branch=master)
+
+MMark (read “em-mark”) is a strict markdown processor for writers. “Strict”
+means that not every input is considered a valid markdown document and parse
+errors are possible and even desirable, because they allow to spot markup
+issues without searching for them in rendered document. If a markdown
+document passes MMark parser, then it'll most certainly produce HTML without
+quirks. This feature makes it a good choice for writers and bloggers.
+
+MMark in its current state features:
+
+* A parser that produces high-quality error messages and does not choke on
+  first parse error. It is capable of reporting many parse errors where
+  makes sense.
+* An extension system allowing to create extensions that alter parsed
+  markdown document in some way. Some of them are available in the
+  [`mmark-ext`](https://hackage.haskell.org/package/mmark-ext) package.
+* A [`lucid`](https://hackage.haskell.org/package/lucid)-based render.
+
+There is also a blog post announcing the project:
+
+https://markkarpov.com/post/announcing-mmark.html
+
+## MMark and Common Mark
+
+MMark mostly tries to follow the Common Mark specification as given here:
+
+https://github.com/jgm/CommonMark
+
+However, due to the fact that we do not allow inputs that do not make sense,
+and also try to guard against common silly mistakes (like writing `##My
+header` and having it rendered as a paragraph starting with hashes) MMark
+obviously can't follow the specification precisely. In particular, parsing
+of inlines differs considerably from Common Mark (see below).
+
+Another difference between Common Mark and MMark is that the latter supports
+more (pun alert) common markdown extensions out-of-the-box. In particular,
+MMark supports:
+
+* parsing of an optional YAML block
+* strikeout using `~~this~~` syntax
+* superscript using `^this^` syntax
+* subscript using `~this~` syntax
+* automatic assignment of ids to headers
+* PHP-style footnotes, e.g. `[^1]` (NOT YET)
+* “pipe” tables (as used on GitHub) (NOT YET)
+
+One do not need to enable or tweak anything for these to work, they are
+built-in features.
+
+### Differences in inline parsing
+
+Emphasis and strong emphasis is an especially hairy topic in the Common Mark
+specification. There are 17 ad-hoc rules defining interaction between `*`
+and `_` -based emphasis and more than half of all Common Mark examples
+(that's about 300) test just this tricky logic.
+
+Not only it is hard to implement (for tools built around markdown too), it's
+hard to understand for humans too. For example, this input:
+
+```
+*(*foo*)*
+```
+
+produces this HTML:
+
+```
+<p><em>(<em>foo</em>)</em></p>
+```
+
+(Note the nested emphasis.)
+
+Could it produce something like this instead?
+
+```
+<p><em>(</em>foo<em>)</em></p>
+```
+
+Well, why not? Without remembering those 17 ad-hoc rules, there going to be
+a lot of tricky cases when a user won't be able to tell how markdown will be
+parsed.
+
+I decided to make parsing of emphasis, strong emphasis, and similar
+constructs like strikethrough, subscript, and superscript more symmetric and
+less ad-hoc. This is a work in progress and I'm not fully satisfied with the
+current approach as it does not allow to express some combinations of
+characters and markup, but in 99% of practical cases it is identical to
+Common Mark, and normal markdown intuitions will work OK for the users.
+
+Let's start by dividing all characters into three groups:
+
+* **Markup characters**, including the following: `*`, `~`, `_`, `` ` ``,
+  `^`, `[`, `]`. These are used for markup and whenever they appear in a
+  document, they must form valid markup constructions. To be used as
+  ordinary punctuation characters they must be backslash escaped.
+
+* **Space characters**, including space, tab, newline, carriage return, and
+  some Unicode space characters.
+
+* **Other characters**, which include all characters not falling into the
+  two groups described above.
+
+**Markup characters** can be “converted” to **other characters** via
+backslash escaping. We'll see how this is useful in a few moments.
+
+We'll call **markdown characters** placed between **space characters** and
+**other characters** *left-flanking delimiter run*. These markup characters
+sort of hang on the left hand side of a word.
+
+Similarly we'll call **markdown characters** placed between **other
+characters** and **space characters** *right-flanking delimiter run*. These
+hang on the right hand side of a word.
+
+Emphasis markup (and other similar things like strikethrough, which we won't
+mention explicitly anymore for brevity) can start only as left-flanking
+delimiter run and end only as right-flanking delimiter run.
+
+This produces a parse error:
+
+```
+*Something * is not right.
+Something __is __ not right.
+```
+
+And this too:
+
+```
+__foo__bar
+```
+
+This means that inter-word emphasis is not supported by this approach. (This
+is a pity, maybe I should adjust something to allow it.)
+
+There is one more tricky thing. In some cases we want to end emphasis and
+have full stop or other punctuation right after it:
+
+```
+Here it *goes*.
+```
+
+You can see that the closing `*` is not in right-flanking position here, and
+so it's a parse error. To avoid this, some punctuation characters that
+normally appear outside of markup were made “transparent” and thus they are
+regarded as white space, so the example above parses correctly and works as
+expected. To put a transparent character inside emphasis, backslash escaping
+is necessary:
+
+```
+We *\(can\)* have it.
+```
+
+Here `(` and `)` are transparent punctuation characters, just like `.`, so
+they must be turned into **other characters** to go inside the emphasis.
+This is a corner case and should not be common in practice.
+
+So far the main limitation of this approach is the pains with inter-word
+markup, as in this example:
+
+```
+**We started to work on the *issue*.**
+```
+
+Should we escape `.` here? On one hand we should, to close `**`. But if we
+do, the closing `*` won't be in right-flanking position anymore. God dammit.
+
+### Other differences
+
+Block-level parsing:
+
+* Headings, thematic breaks, code blocks should be separated from paragraphs
+  by at least one empty line. This makes the parser a lot simpler and forces
+  markdown sources to be in a more readable form too.
+* If a line starts with hash signs it is expected to be a valid *non-empty*
+  header (level 1–6 inclusive). If you want to start a paragraph with
+  hashes, just escape the first hash with backslash and that will be enough.
+* Fenced code blocks must be explicitly closed by a closing fence. They are
+  not closed by the end of document or by start of another block.
+
+Inline-level parsing:
+
+* MMark does not support hard line breaks represented as double space before
+  newline. Nevertheless, hard line breaks in the form of backslash before
+  newline are supported (these are more explicit too).
+* All URI references (in links, images, autolinks, etc.) are parsed as per
+  RFC 3986, no special escaping is supported. In addition to that, when a
+  URI reference in not enclosed with `<` and `>`, then closing parenthesis
+  character `)` is not considered part of URI (use `<uri>` syntax if you
+  want closing parenthesis as part of a URI).
+* Putting links in text of another link is not allowed, i.e. no nested links
+  is possible.
+* Putting images in description of other images is not allowed (similarly to
+  the situation with links).
+
+Not-yet-implemented things:
+
+* Separate declaration of image's source and title is not (yet?) supported.
+* Blockquotes are not implemented yet.
+* Lists (unordered and ordered) are not implemented yet.
+* Setext headings are not implemented yet.
+* Reference links are not implemented yet.
+* HTML blocks are not implemented yet.
+* HTML inlines are not implemented yet.
+* Entity and numeric character references are not implemented yet.
+
+### Additional information about MMark-specific extensions
+
+* YAML block must start with three hyphens `---` and end with three hyphens
+  `---`. It can only be placed at the beginning of a markdown document.
+  Trailing white space after the `---** sequences is tolerated.
+
+## Contribution
+
+Issues, bugs, and questions may be reported in [the GitHub issue tracker for
+this project](https://github.com/mrkkrp/mmark/issues).
+
+Pull requests are also welcome and will be reviewed quickly.
+
+## License
+
+Copyright © 2017 Mark Karpov
+
+Distributed under BSD 3 clause license.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main (main) where
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/Text/MMark.hs b/Text/MMark.hs
new file mode 100644
--- /dev/null
+++ b/Text/MMark.hs
@@ -0,0 +1,114 @@
+-- |
+-- Module      :  Text.MMark
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- MMark (read “em-mark”) is a strict markdown processor for writers.
+-- “Strict” means that not every input is considered a valid markdown
+-- document and parse errors are possible and even desirable, because they
+-- allow to spot markup issues without searching for them in rendered
+-- document. If a markdown document passes MMark parser, then it'll most
+-- certainly produce HTML without quirks. This feature makes it a good
+-- choice for writers and bloggers.
+--
+-- === MMark and Common Mark
+--
+-- MMark mostly tries to follow the Common Mark specification as given here:
+--
+-- <https://github.com/jgm/CommonMark>
+--
+-- However, due to the fact that we do not allow inputs that do not make
+-- sense, and also try to guard against common silly mistakes (like writing
+-- @##My header@ and having it rendered as a paragraph starting with hashes)
+-- MMark obviously can't follow the specification precisely. In particular,
+-- parsing of inlines differs considerably from Common Mark.
+--
+-- Another difference between Common Mark and MMark is that the latter
+-- supports more (pun alert) common markdown extensions out-of-the-box. In
+-- particular, MMark supports:
+--
+--     * parsing of an optional YAML block
+--     * strikeout using @~~this~~@ syntax
+--     * superscript using @^this^@ syntax
+--     * subscript using @~this~@ syntax
+--     * automatic assignment of ids to headers
+--     * PHP-style footnotes, e.g. @[^1]@ (NOT YET)
+--     * “pipe” tables (as used on GitHub) (NOT YET)
+--
+-- One do not need to enable or tweak anything for these to work, they are
+-- built-in features.
+--
+-- The readme contains a more detailed description of differences between
+-- Common Mark and MMark.
+--
+-- === How to use the library
+--
+-- The module is intended to be imported qualified:
+--
+-- > import Text.MMark (MMark)
+-- > import qualified Text.MMark as MMark
+--
+-- Working with MMark happens in three stages:
+--
+--     1. Parsing of markdown document.
+--     2. Applying extensions, which optionally may require scanning of
+--        previously parsed document (for example to build a table of
+--        contents).
+--     3. Rendering of HTML document.
+--
+-- The structure of the documentation below corresponds to these stages and
+-- should clarify the details.
+--
+-- === Other modules of interest
+--
+-- This module contains all the “core” functionality you may need. However,
+-- one of the main selling points of MMark is that it's possible to write
+-- your own extensions which stay highly composable (if done right), so
+-- proliferation of third-party extensions is to be expected and encouraged.
+-- To write an extension of your own import the "Text.MMark.Extension"
+-- module, which has some documentation focusing on extension writing.
+
+module Text.MMark
+  ( -- * Parsing
+    MMark
+  , MMarkErr (..)
+  , parse
+  , parseErrorsPretty
+    -- * Extensions
+  , Extension
+  , useExtension
+  , useExtensions
+    -- * Scanning
+  , runScanner
+  , projectYaml
+    -- * Rendering
+  , render )
+where
+
+import Data.Aeson
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Text (Text)
+import Text.MMark.Internal
+import Text.MMark.Parser
+import Text.Megaparsec (ParseError (..), parseErrorPretty_, mkPos)
+
+-- | Extract contents of optional YAML block that may have been parsed.
+
+projectYaml :: MMark -> Maybe Value
+projectYaml = mmarkYaml
+
+-- | Pretty-print a collection of parse errors returned from 'parse'.
+--
+-- __Pro tip__: if you would like to pretty-print a single 'ParseError', use
+-- @'parseErrorPretty_' ('mkPos' 4)@, because Common Mark suggests that we
+-- should assume tab width 4, and that's what we do in the parser.
+
+parseErrorsPretty
+  :: Text              -- ^ Original input for parser
+  -> NonEmpty (ParseError Char MMarkErr) -- ^ Collection of parse errors
+  -> String            -- ^ Result of pretty-printing
+parseErrorsPretty input = concatMap (parseErrorPretty_ (mkPos 4) input)
diff --git a/Text/MMark/Extension.hs b/Text/MMark/Extension.hs
new file mode 100644
--- /dev/null
+++ b/Text/MMark/Extension.hs
@@ -0,0 +1,113 @@
+-- |
+-- Module      :  Text.MMark.Extension
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- This module provides building blocks for extension creation.
+--
+-- We suggest using a qualified import, like this:
+--
+-- > import Text.MMark.Extension (Bni, Block (..), Inline (..))
+-- > import qualified Text.MMark.Extension as Ext
+--
+-- === Details about extensions
+--
+-- There are four kinds of extension-producing functions. They correspond
+-- internally to four functions that are applied to the parsed document in
+-- turn:
+--
+--     * 'blockTrans' is applied first, as it's quite general and can change
+--       block-level structure of document as well as inline-level
+--       structure.
+--     * 'inlineTrans' is applied to every inline in the document obtained
+--       in the previous step.
+--     * 'inlineRender' is applied to every inline; this function produces
+--       HTML rendition of the inlines and we also preserve the original
+--       inline so 'blockRender' can look at it (sometimes it is useful).
+--     * 'blockRender' is applied to every block to obtain HTML rendition of
+--       the whole document.
+--
+-- When one combines different extensions, extensions of the same kind get
+-- fused together into a single function. This allows for faster processing
+-- in the end.
+
+{-# LANGUAGE RankNTypes #-}
+
+module Text.MMark.Extension
+  ( -- * Extension construction
+    Extension
+    -- ** Block-level manipulation
+  , Bni
+  , Block (..)
+  , blockTrans
+  , blockRender
+  , Ois
+  , getOis
+    -- ** Inline-level manipulation
+  , Inline (..)
+  , inlineTrans
+  , inlineRender
+    -- * Scanner construction
+  , scanner
+    -- * Utils
+  , asPlainText
+  , headerId
+  , headerFragment )
+where
+
+import Data.Monoid hiding ((<>))
+import Lucid
+import Text.MMark.Internal
+import qualified Control.Foldl as L
+
+-- | Create an extension that performs a transformation on 'Block's of
+-- markdown document.
+
+blockTrans :: (Bni -> Bni) -> Extension
+blockTrans f = mempty { extBlockTrans = Endo f }
+
+-- | Create an extension that replaces or augments rendering of 'Block's of
+-- markdown document. The argument of 'blockRender' will be given the
+-- rendering function constructed so far @'Block' ('Ois', 'Html' ()) ->
+-- 'Html' ()@ as well as an actual block to render—@'Block' ('Ois', 'Html'
+-- ())@. The user can then decide whether to replace\/reuse that function to
+-- get the final rendering of the type @'Html' ()@.
+--
+-- The argument of 'blockRender' can also be thought of as a function that
+-- transforms the rendering function constructed so far:
+--
+-- > (Block (Ois, Html ()) -> Html ()) -> (Block (Ois, Html ()) -> Html ())
+--
+-- See also: 'Ois' and 'getOis'.
+
+blockRender
+  :: ((Block (Ois, Html ()) -> Html ()) -> Block (Ois, Html ()) -> Html ())
+  -> Extension
+blockRender f = mempty { extBlockRender = Render f }
+
+-- | Create an extension that performs a transformation on 'Inline'
+-- components in entire markdown document.
+
+inlineTrans :: (Inline -> Inline) -> Extension
+inlineTrans f = mempty { extInlineTrans = Endo f }
+
+-- | Create an extension that replaces or augments rendering of 'Inline's of
+-- markdown document. This works like 'blockRender'.
+
+inlineRender
+  :: ((Inline -> Html ()) -> Inline -> Html ())
+  -> Extension
+inlineRender f = mempty { extInlineRender = Render f }
+
+-- | Create a 'L.Fold' from an initial state and a folding function.
+
+scanner
+  :: a                 -- ^ Initial state
+  -> (a -> Bni -> a)   -- ^ Folding function
+  -> L.Fold Bni a      -- ^ Resulting 'L.Fold'
+scanner a f = L.Fold f a id
+{-# INLINE scanner #-}
diff --git a/Text/MMark/Internal.hs b/Text/MMark/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Text/MMark/Internal.hs
@@ -0,0 +1,419 @@
+-- |
+-- Module      :  Text.MMark.Internal
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Internal definitions.
+
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveFoldable     #-}
+{-# LANGUAGE DeriveFunctor      #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE LambdaCase         #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RankNTypes         #-}
+{-# LANGUAGE RecordWildCards    #-}
+
+module Text.MMark.Internal
+  ( -- * Types
+    MMark (..)
+  , Extension (..)
+  , Bni
+  , Block (..)
+  , Inline (..)
+    -- * Extensions
+  , runScanner
+  , useExtension
+  , useExtensions
+    -- * Renders
+  , render
+  , Ois
+  , getOis
+  , Render (..)
+  , defaultBlockRender
+  , defaultInlineRender
+    -- * Utils
+  , asPlainText
+  , headerId
+  , headerFragment )
+where
+
+import Control.Arrow
+import Control.DeepSeq
+import Control.Monad
+import Data.Aeson
+import Data.Char (isSpace)
+import Data.Data (Data)
+import Data.Function (on)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Monoid hiding ((<>))
+import Data.Semigroup
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import GHC.Generics
+import Lucid
+import Text.URI (URI (..))
+import qualified Control.Foldl as L
+import qualified Data.Text     as T
+import qualified Text.URI      as URI
+
+----------------------------------------------------------------------------
+-- Types
+
+-- | Representation of complete markdown document. You can't look inside of
+-- 'MMark' on purpose. The only way to influence an 'MMark' document you
+-- obtain as a result of parsing is via the extension mechanism.
+
+data MMark = MMark
+  { mmarkYaml :: Maybe Value
+    -- ^ Parsed YAML document at the beginning (optional)
+  , mmarkBlocks :: [Bni]
+    -- ^ Actual contents of the document
+  , mmarkExtension :: Extension
+    -- ^ Extension specifying how to process and render the blocks
+  }
+
+instance NFData MMark where
+  rnf MMark {..} = rnf mmarkYaml `seq` rnf mmarkBlocks
+
+-- | An extension. You can apply extensions with 'useExtension' and
+-- 'useExtensions' functions. The "Text.MMark.Extension" module provides
+-- tools for extension creation.
+--
+-- Note that 'Extension' is an instance of 'Semigroup' and 'Monoid', i.e.
+-- you can combine several extensions into one. Since the @('<>')@ operator
+-- is right-associative and 'mconcat' is a right fold under the hood, the
+-- expression
+--
+-- > l <> r
+--
+-- means that the extension @r@ will be applied before the extension @l@,
+-- similar to how 'Endo' works. This may seem counter-intuitive, but only
+-- with this logic we get consistency of ordering with more complex
+-- expressions:
+--
+-- > e2 <> e1 <> e0 == e2 <> (e1 <> e0)
+--
+-- Here, @e0@ will be applied first, then @e1@, then @e2@. The same applies
+-- to expressions involving 'mconcat'—extensions closer to beginning of the
+-- list passed to 'mconcat' will be applied later.
+
+data Extension = Extension
+  { extBlockTrans :: Endo Bni
+    -- ^ Block transformation
+  , extBlockRender :: Render (Block (Ois, Html ()))
+    -- ^ Block render
+  , extInlineTrans :: Endo Inline
+    -- ^ Inline transformation
+  , extInlineRender :: Render Inline
+    -- ^ Inline render
+  }
+
+instance Semigroup Extension where
+  x <> y = Extension
+    { extBlockTrans   = on (<>) extBlockTrans   x y
+    , extBlockRender  = on (<>) extBlockRender  x y
+    , extInlineTrans  = on (<>) extInlineTrans  x y
+    , extInlineRender = on (<>) extInlineRender x y }
+
+instance Monoid Extension where
+  mempty = Extension
+    { extBlockTrans   = mempty
+    , extBlockRender  = mempty
+    , extInlineTrans  = mempty
+    , extInlineRender = mempty }
+  mappend = (<>)
+
+-- | A shortcut for the frequently used type @'Block' ('NonEmpty'
+-- 'Inline')@.
+
+type Bni = Block (NonEmpty Inline)
+
+-- | We can think of a markdown document as a collection of
+-- blocks—structural elements like paragraphs, block quotations, lists,
+-- headings, thematic breaks, and code blocks. Some blocks (like block
+-- quotes and list items) contain other blocks; others (like headings and
+-- paragraphs) contain inline content, see 'Inline'.
+--
+-- We can divide blocks into two types: container blocks, which can contain
+-- other blocks, and leaf blocks, which cannot.
+
+data Block a
+  = ThematicBreak
+    -- ^ Thematic break, leaf block
+  | Heading1 a
+    -- ^ Heading (level 1), leaf block
+  | Heading2 a
+    -- ^ Heading (level 2), leaf block
+  | Heading3 a
+    -- ^ Heading (level 3), leaf block
+  | Heading4 a
+    -- ^ Heading (level 4), leaf block
+  | Heading5 a
+    -- ^ Heading (level 5), leaf block
+  | Heading6 a
+    -- ^ Heading (level 6), leaf block
+  | CodeBlock (Maybe Text) Text
+    -- ^ Code block, leaf block with info string and contents
+  | Paragraph a
+    -- ^ Paragraph, leaf block
+  | Blockquote [Block a]
+    -- ^ Blockquote container block
+  | OrderedList (NonEmpty [Block a])
+    -- ^ Ordered list, container block
+  | UnorderedList (NonEmpty [Block a])
+    -- ^ Unordered list, container block
+  | Naked a
+    -- ^ Naked content, without an enclosing tag
+  deriving (Show, Eq, Ord, Data, Typeable, Generic, Functor, Foldable)
+
+instance NFData a => NFData (Block a)
+
+-- | Inline markdown content.
+
+data Inline
+  = Plain Text
+    -- ^ Plain text
+  | LineBreak
+    -- ^ Line break (hard)
+  | Emphasis (NonEmpty Inline)
+    -- ^ Emphasis
+  | Strong (NonEmpty Inline)
+    -- ^ Strong emphasis
+  | Strikeout (NonEmpty Inline)
+    -- ^ Strikeout
+  | Subscript (NonEmpty Inline)
+    -- ^ Subscript
+  | Superscript (NonEmpty Inline)
+    -- ^ Superscript
+  | CodeSpan Text
+    -- ^ Code span
+  | Link (NonEmpty Inline) URI (Maybe Text)
+    -- ^ Link with text, destination, and optionally title
+  | Image (NonEmpty Inline) URI (Maybe Text)
+    -- ^ Image with description, URL, and optionally title
+  deriving (Show, Eq, Ord, Data, Typeable, Generic)
+
+instance NFData Inline
+
+----------------------------------------------------------------------------
+-- Extensions
+
+-- | Apply an 'Extension' to an 'MMark' document. The order in which you
+-- apply 'Extension's /does matter/. Extensions you apply first take effect
+-- first. The extension system is designed in such a way that in many cases
+-- the order doesn't matter, but sometimes the difference is important.
+
+useExtension :: Extension -> MMark -> MMark
+useExtension ext mmark =
+  mmark { mmarkExtension = ext <> mmarkExtension mmark }
+
+-- | Apply several 'Extension's to an 'MMark' document.
+--
+-- This is a simple shortcut:
+--
+-- > useExtensions exts = useExtension (mconcat exts)
+--
+-- As mentioned in the docs for 'useExtension', the order in which you apply
+-- extensions matters. Extensions closer to beginning of the list are
+-- applied later, i.e. the last extension in the list is applied first.
+
+useExtensions :: [Extension] -> MMark -> MMark
+useExtensions exts = useExtension (mconcat exts)
+
+-- | Scan an 'MMark' document efficiently in one pass. This uses the
+-- excellent 'L.Fold' type, which see.
+--
+-- Take a look at the "Text.MMark.Extension" module if you want to create
+-- scanners of your own.
+
+runScanner
+  :: MMark             -- ^ Document to scan
+  -> L.Fold Bni a      -- ^ 'L.Fold' to use
+  -> a                 -- ^ Result of scanning
+runScanner MMark {..} f = L.fold f mmarkBlocks
+{-# INLINE runScanner #-}
+
+----------------------------------------------------------------------------
+-- Renders
+
+-- | Render a 'MMark' markdown document. You can then render @'Html' ()@ to
+-- various things:
+--
+--     * to lazy 'Data.Taxt.Lazy.Text' with 'renderText'
+--     * to lazy 'Data.ByteString.Lazy.ByteString' with 'renderBS'
+--     * directly to file with 'renderToFile'
+
+render :: MMark -> Html ()
+render MMark {..} =
+  mapM_ produceBlock mmarkBlocks
+  where
+    Extension {..} = mmarkExtension
+    produceBlock   = applyBlockRender extBlockRender
+      . fmap ((Ois &&& mapM_ (applyInlineRender extInlineRender)) .
+              fmap  (appEndo extInlineTrans))
+      . appEndo extBlockTrans
+
+-- | A wrapper for “originial inlines”. Source inlines are wrapped in this
+-- during rendering of inline components and then it's available to block
+-- render, but only for inspection. Altering of 'Ois' is not possible
+-- because the user cannot construct a value of the 'Ois' type, she can only
+-- inspect it with 'getOis'.
+
+newtype Ois = Ois (NonEmpty Inline)
+
+-- | Project @'NonEmpty' 'Inline'@ from 'Ois'.
+
+getOis :: Ois -> NonEmpty Inline
+getOis (Ois inlines) = inlines
+
+-- | An internal type that captures the extensible rendering process we use.
+-- 'Render' has a function inside which transforms a rendering function of
+-- the type @a -> Html ()@.
+
+newtype Render a = Render
+  { getRender :: (a -> Html ()) -> a -> Html () }
+
+instance Semigroup (Render a) where
+  Render f <> Render g = Render $ \h -> f (g h)
+
+instance Monoid (Render a) where
+  mempty  = Render id
+  mappend = (<>)
+
+-- | Apply a 'Render' to a given @'Block' 'Html' ()@.
+
+applyBlockRender
+  :: Render (Block (Ois, Html ()))
+  -> Block (Ois, Html ())
+  -> Html ()
+applyBlockRender r = getRender r defaultBlockRender
+
+-- | The default 'Block' render. Note that it does not care about what we
+-- have rendered so far because it always starts rendering. Thus it's OK to
+-- just pass it something dummy as the second argument of the inner
+-- function.
+
+defaultBlockRender :: Block (Ois, Html ()) -> Html ()
+defaultBlockRender = \case
+  ThematicBreak ->
+    hr_ [] >> newline
+  Heading1 (h,html) ->
+    h1_ (mkId h) html >> newline
+  Heading2 (h,html) ->
+    h2_ (mkId h) html >> newline
+  Heading3 (h,html) ->
+    h3_ (mkId h) html >> newline
+  Heading4 (h,html) ->
+    h4_ (mkId h) html >> newline
+  Heading5 (h,html) ->
+    h5_ (mkId h) html >> newline
+  Heading6 (h,html) ->
+    h6_ (mkId h) html >> newline
+  CodeBlock infoString txt -> do
+    let f x = class_ $ "language-" <> T.takeWhile (not . isSpace) x
+    pre_ $ code_ (maybe [] (pure . f) infoString) (toHtml txt)
+    newline
+  Paragraph (_,html) ->
+    p_ html >> newline
+  Blockquote blocks ->
+    blockquote_ (mapM_ defaultBlockRender blocks)
+  OrderedList items -> do
+    ol_ $ do
+      newline
+      forM_ items $ \x -> do
+        li_ (mapM_ defaultBlockRender x)
+        newline
+    newline
+  UnorderedList items -> do
+    ul_ $ do
+      newline
+      forM_ items $ \x -> do
+        li_ (mapM_ defaultBlockRender x)
+        newline
+    newline
+  Naked (_,html) ->
+    html
+  where
+    mkId (Ois x) = [id_ (headerId x)]
+
+-- | Apply a render to a given 'Inline'.
+
+applyInlineRender :: Render Inline -> Inline -> Html ()
+applyInlineRender r = getRender r defaultInlineRender
+
+-- | The default render for 'Inline' elements. Comments about
+-- 'defaultBlockRender' apply here just as well.
+
+defaultInlineRender :: Inline -> Html ()
+defaultInlineRender = \case
+  Plain txt ->
+    toHtml txt
+  LineBreak ->
+    br_ [] >> newline
+  Emphasis inner ->
+    em_ (mapM_ defaultInlineRender inner)
+  Strong inner ->
+    strong_ (mapM_ defaultInlineRender inner)
+  Strikeout inner ->
+    del_ (mapM_ defaultInlineRender inner)
+  Subscript inner ->
+    sub_ (mapM_ defaultInlineRender inner)
+  Superscript inner ->
+    sup_ (mapM_ defaultInlineRender inner)
+  CodeSpan txt ->
+    code_ (toHtmlRaw txt)
+  Link inner dest mtitle ->
+    let title = maybe [] (pure . title_) mtitle
+    in a_ (href_ (URI.render dest) : title) (mapM_ defaultInlineRender inner)
+  Image desc src mtitle ->
+    let title = maybe [] (pure . title_) mtitle
+    in img_ (alt_ (asPlainText desc) : src_ (URI.render src) : title)
+
+-- | HTML containing a newline.
+
+newline :: Html ()
+newline = "\n"
+
+----------------------------------------------------------------------------
+-- Utils
+
+-- | Convert a non-empty collection of 'Inline's into their plain text
+-- representation. This is used e.g. to render image descriptions.
+
+asPlainText :: NonEmpty Inline -> Text
+asPlainText = foldMap $ \case
+  Plain      txt -> txt
+  LineBreak      -> "\n"
+  Emphasis    xs -> asPlainText xs
+  Strong      xs -> asPlainText xs
+  Strikeout   xs -> asPlainText xs
+  Subscript   xs -> asPlainText xs
+  Superscript xs -> asPlainText xs
+  CodeSpan   txt -> txt
+  Link    xs _ _ -> asPlainText xs
+  Image   xs _ _ -> asPlainText xs
+
+-- | Generate value of id attribute for a given header. This is used during
+-- rendering and also can be used to get id of a header for linking to it in
+-- extensions.
+--
+-- See also: 'headerFragment'.
+
+headerId :: NonEmpty Inline -> Text
+headerId = T.intercalate "-" . T.words . T.toLower . asPlainText
+
+-- | Generate a 'URI' with just fragment from its textual representation.
+-- Useful for getting URL from id of a header.
+
+headerFragment :: Text -> URI
+headerFragment fragment = URI
+  { uriScheme    = Nothing
+  , uriAuthority = Left False
+  , uriPath      = []
+  , uriQuery     = []
+  , uriFragment  = URI.mkFragment fragment }
diff --git a/Text/MMark/Parser.hs b/Text/MMark/Parser.hs
new file mode 100644
--- /dev/null
+++ b/Text/MMark/Parser.hs
@@ -0,0 +1,762 @@
+-- |
+-- Module      :  Text.MMark.Parser
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- MMark parser.
+
+{-# LANGUAGE BangPatterns       #-}
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DataKinds          #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE GADTs              #-}
+{-# LANGUAGE LambdaCase         #-}
+{-# LANGUAGE MultiWayIf         #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE TypeFamilies       #-}
+
+module Text.MMark.Parser
+  ( MMarkErr (..)
+  , parse )
+where
+
+import Control.Applicative
+import Control.DeepSeq
+import Control.Monad
+import Control.Monad.State.Strict
+import Data.Bifunctor (Bifunctor (..))
+import Data.Data (Data)
+import Data.Default.Class
+import Data.List.NonEmpty (NonEmpty (..), (<|))
+import Data.Maybe (isNothing, fromJust, fromMaybe)
+import Data.Semigroup ((<>))
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import Data.Void
+import GHC.Generics
+import Text.MMark.Internal
+import Text.Megaparsec hiding (parse)
+import Text.Megaparsec.Char hiding (eol)
+import Text.URI (URI)
+import qualified Control.Applicative.Combinators.NonEmpty as NE
+import qualified Data.Char                  as Char
+import qualified Data.List.NonEmpty         as NE
+import qualified Data.Set                   as E
+import qualified Data.Text                  as T
+import qualified Data.Text.Encoding         as TE
+import qualified Data.Yaml                  as Yaml
+import qualified Text.Email.Validate        as Email
+import qualified Text.Megaparsec.Char.Lexer as L
+import qualified Text.URI                   as URI
+
+----------------------------------------------------------------------------
+-- Data types
+
+-- | Parser type we use internally.
+
+type Parser = Parsec MMarkErr Text
+
+-- | MMark custom parse errors.
+
+data MMarkErr
+  = YamlParseError String
+    -- ^ YAML error that occurred during parsing of a YAML block
+  | NonFlankingDelimiterRun (NonEmpty Char)
+    -- ^ This delimiter run should be in left- or right- flanking position
+  deriving (Eq, Ord, Show, Read, Generic, Typeable, Data)
+
+instance ShowErrorComponent MMarkErr where
+  showErrorComponent = \case
+    YamlParseError str ->
+      "YAML parse error: " ++ str
+    NonFlankingDelimiterRun dels ->
+      showTokens dels ++ " should be in left- or right- flanking position"
+
+instance NFData MMarkErr
+
+-- | Parser type for inlines.
+
+type IParser = StateT CharType (Parsec MMarkErr Text)
+
+-- | 'Inline' source pending parsing.
+
+data Isp = Isp SourcePos Text
+  deriving (Eq, Ord, Show)
+
+-- | Type of last seen character.
+
+data CharType
+  = SpaceChar          -- ^ White space
+  | LeftFlankingDel    -- ^ Left flanking delimiter
+  | RightFlankingDel   -- ^ Right flaking delimiter
+  | OtherChar          -- ^ Other character
+  deriving (Eq, Ord, Show)
+
+-- | Frame that describes where we are in parsing inlines.
+
+data InlineFrame
+  = EmphasisFrame      -- ^ Emphasis with asterisk @*@
+  | EmphasisFrame_     -- ^ Emphasis with underscore @_@
+  | StrongFrame        -- ^ Strong emphasis with asterisk @**@
+  | StrongFrame_       -- ^ Strong emphasis with underscore @__@
+  | StrikeoutFrame     -- ^ Strikeout
+  | SubscriptFrame     -- ^ Subscript
+  | SuperscriptFrame   -- ^ Superscript
+  deriving (Eq, Ord, Show)
+
+-- | State of inline parsing that specifies whether we expect to close one
+-- frame or there is a possibility to close one of two alternatives.
+
+data InlineState
+  = SingleFrame InlineFrame             -- ^ One frame to be closed
+  | DoubleFrame InlineFrame InlineFrame -- ^ Two frames to be closed
+  deriving (Eq, Ord, Show)
+
+-- | Configuration in inline parser.
+
+data InlineConfig = InlineConfig
+  { iconfigAllowEmpty :: !Bool
+    -- ^ Whether to accept empty inline blocks
+  , iconfigAllowLinks :: !Bool
+    -- ^ Whether to parse links
+  , iconfigAllowImages :: !Bool
+    -- ^ Whether to parse images
+  }
+
+instance Default InlineConfig where
+  def = InlineConfig
+    { iconfigAllowEmpty  = True
+    , iconfigAllowLinks  = True
+    , iconfigAllowImages = True
+    }
+
+-- | A shortcut type synonym for sub-parsers that may fail and we can
+-- recover from the failure.
+
+type E = Either (ParseError Char MMarkErr)
+
+----------------------------------------------------------------------------
+-- Block parser
+
+-- | Parse a markdown document in the form of a strict 'Text' value and
+-- either report parse errors or return a 'MMark' document. Note that the
+-- parser has the ability to report multiple parse errors at once.
+
+parse
+  :: String
+     -- ^ File name (only to be used in error messages), may be empty
+  -> Text
+     -- ^ Input to parse
+  -> Either (NonEmpty (ParseError Char MMarkErr)) MMark
+     -- ^ Parse errors or parsed document
+parse file input =
+  case runParser ((,) <$> optional pYamlBlock <*> pBlocks) file input of
+    -- NOTE This parse error only happens when document structure on block
+    -- level cannot be parsed even with recovery, which should not normally
+    -- happen.
+    Left err -> Left (nes err)
+    Right (myaml, blocks) ->
+      let parsed = doInline <$> blocks
+          doInline = \case
+            Left err -> Naked (Left err)
+            Right x  -> first (replaceEof "end of inline block")
+              . runIsp (pInlines def <* eof) <$> x
+          getErrs (Left e) es = e : es
+          getErrs _        es = es
+          fromRight (Right x) = x
+          fromRight _         =
+            error "Text.MMark.Parser.parse: the impossible happened"
+      in case NE.nonEmpty (foldMap (foldr getErrs []) parsed) of
+           Nothing -> Right MMark
+             { mmarkYaml      = myaml
+             , mmarkBlocks    = fmap fromRight <$> parsed
+             , mmarkExtension = mempty }
+           Just es -> Left es
+
+pYamlBlock :: Parser Yaml.Value
+pYamlBlock = do
+  dpos <- getPosition
+  string "---" *> sc' *> eol
+  let go = do
+        l <- takeWhileP Nothing notNewline
+        void (optional eol)
+        e <- atEnd
+        if e || T.stripEnd l == "---"
+          then return []
+          else (l :) <$> go
+  ls <- go
+  case (Yaml.decodeEither . TE.encodeUtf8 . T.intercalate "\n") ls of
+    Left err' -> do
+      let (apos, err) = splitYamlError (sourceName dpos) err'
+      setPosition (fromMaybe dpos apos)
+      (fancyFailure . E.singleton . ErrorCustom . YamlParseError) err
+    Right v ->
+      return v
+
+pBlocks :: Parser [E (Block Isp)]
+pBlocks = do
+  setTabWidth (mkPos 4)
+  sc *> manyTill pBlock eof
+
+pBlock :: Parser (E (Block Isp))
+pBlock = choice
+  [ try (pure <$> pThematicBreak)
+  , pAtxHeading
+  , pure <$> pFencedCodeBlock
+  , try (pure <$> pIndentedCodeBlock)
+  , pure <$> pParagraph ]
+
+pThematicBreak :: Parser (Block Isp)
+pThematicBreak = do
+  void casualLevel
+  l <- lookAhead nonEmptyLine
+  if isThematicBreak l
+    then ThematicBreak <$ nonEmptyLine <* sc
+    else empty
+
+pAtxHeading :: Parser (E (Block Isp))
+pAtxHeading = do
+  (void . lookAhead . try) start
+  withRecovery recover $ do
+    hlevel <- length <$> start
+    sc1'
+    ispPos <- getPosition
+    r <- someTill (satisfy notNewline <?> "heading character") . try $
+      optional (sc1' *> some (char '#') *> sc') *> (eof <|> eol)
+    let toBlock = case hlevel of
+          1 -> Heading1
+          2 -> Heading2
+          3 -> Heading3
+          4 -> Heading4
+          5 -> Heading5
+          _ -> Heading6
+    (Right . toBlock) (Isp ispPos (T.strip (T.pack r))) <$ sc
+  where
+    start = casualLevel *> count' 1 6 (char '#')
+    recover err =
+      Left err <$ takeWhileP Nothing notNewline <* optional eol
+
+pFencedCodeBlock :: Parser (Block Isp)
+pFencedCodeBlock = do
+  level <- casualLevel
+  let p ch = try $ do
+        void $ count 3 (char ch)
+        n  <- (+ 3) . length <$> many (char ch)
+        ml <- optional (T.strip <$> someEscapedWith notNewline <?> "info string")
+        guard (maybe True (not . T.any (== '`')) ml)
+        return
+          (ch, n,
+             case ml of
+               Nothing -> Nothing
+               Just l  ->
+                 if T.null l
+                   then Nothing
+                   else Just l)
+  (ch, n, infoString) <- (p '`' <|> p '~') <* eol
+  let content = label "code block content" (option "" nonEmptyLine <* eol)
+      closingFence = try . label "closing code fence" $ do
+        void casualLevel'
+        void $ count n (char ch)
+        (void . many . char) ch
+        sc'
+        eof <|> eol
+  ls <- manyTill content closingFence
+  CodeBlock infoString (assembleCodeBlock level ls) <$ sc
+
+pIndentedCodeBlock :: Parser (Block Isp)
+pIndentedCodeBlock = do
+  initialIndent <- codeBlockLevel
+  let go ls = do
+        immediate <- lookAhead (True <$ try codeBlockLevel' <|> pure False)
+        eventual  <- lookAhead (True <$ try codeBlockLevel  <|> pure False)
+        if not immediate && not eventual
+          then return ls
+          else do
+            l        <- option "" nonEmptyLine
+            continue <- eol'
+            if continue
+              then go (l:ls)
+              else return (l:ls)
+      -- NOTE This is a bit unfortunate, but it's difficult to guarantee
+      -- that preceding space is not yet consumed when we get to
+      -- interpreting input as an indented code block, so we need to restore
+      -- the space this way.
+      f x      = T.replicate (unPos initialIndent - 1) " " <> x
+      g []     = []
+      g (x:xs) = f x : xs
+  ls <- g . reverse . dropWhile isBlank <$> go []
+  CodeBlock Nothing (assembleCodeBlock (mkPos 5) ls) <$ sc
+
+pParagraph :: Parser (Block Isp)
+pParagraph = do
+  void casualLevel
+  startPos <- getPosition
+  let go = do
+        ml <- lookAhead (optional nonEmptyLine)
+        case ml of
+          Nothing -> return []
+          Just l ->
+            if isBlank l
+              then return []
+              else do
+                void nonEmptyLine
+                continue <- eol'
+                (l :) <$> if continue then go else return []
+  l        <- nonEmptyLine
+  continue <- eol'
+  ls       <- if continue then go else return []
+  Paragraph (Isp startPos (assembleParagraph (l:ls))) <$ sc
+
+----------------------------------------------------------------------------
+-- Inline parser
+
+-- | Run a given parser on 'Isp'.
+
+runIsp
+  :: IParser a         -- ^ The parser to run
+  -> Isp               -- ^ Input for the parser
+  -> Either (ParseError Char MMarkErr) a -- ^ Result of parsing
+runIsp p (Isp startPos input) =
+  snd (runParser' (evalStateT p SpaceChar) pst)
+  where
+    pst = State
+      { stateInput           = input
+      , statePos             = nes startPos
+      , stateTokensProcessed = 0
+      , stateTabWidth        = mkPos 4 }
+
+pInlines :: InlineConfig -> IParser (NonEmpty Inline)
+pInlines InlineConfig {..} =
+  if iconfigAllowEmpty
+    then nes (Plain "") <$ eof <|> stuff
+    else stuff
+  where
+    stuff = NE.some . label "inline content" . choice $
+      [ pCodeSpan                                  ] <>
+      [ pInlineLink           | iconfigAllowLinks  ] <>
+      [ pImage                | iconfigAllowImages ] <>
+      [ try (angel pAutolink) | iconfigAllowLinks  ] <>
+      [ pEnclosedInline
+      , try pHardLineBreak
+      , pPlain ]
+    angel = between (char '<') (char '>')
+
+pCodeSpan :: IParser Inline
+pCodeSpan = do
+  n <- try (length <$> some (char '`'))
+  let finalizer = try $ do
+        void $ count n (char '`')
+        notFollowedBy (char '`')
+  r <- CodeSpan . collapseWhiteSpace . T.concat <$>
+    manyTill (label "code span content" $
+               takeWhile1P Nothing (== '`') <|>
+               takeWhile1P Nothing (/= '`'))
+      finalizer
+  put OtherChar
+  return r
+
+pInlineLink :: IParser Inline
+pInlineLink = do
+  xs     <- between (char '[') (char ']') $
+    pInlines def { iconfigAllowLinks = False }
+  void (char '(') <* sc
+  dest   <- pUri
+  mtitle <- optional (sc1 *> pTitle)
+  sc <* char ')'
+  put OtherChar
+  return (Link xs dest mtitle)
+
+pImage :: IParser Inline
+pImage = do
+  let nonEmptyDesc = char '!' *> between (char '[') (char ']')
+        (pInlines def { iconfigAllowImages = False })
+  alt    <- nes (Plain "") <$ string "![]" <|> nonEmptyDesc
+  void (char '(') <* sc
+  src    <- pUri
+  mtitle <- optional (sc1 *> pTitle)
+  sc <* char ')'
+  put OtherChar
+  return (Image alt src mtitle)
+
+pUri :: IParser URI
+pUri = do
+  uri <- between (char '<') (char '>') URI.parser <|> naked
+  put OtherChar
+  return uri
+  where
+    naked = do
+      startPos <- getPosition
+      input    <- takeWhileP Nothing $ \x ->
+        not (isSpaceN x || x == ')')
+      let pst = State
+            { stateInput           = input
+            , statePos             = nes startPos
+            , stateTokensProcessed = 0
+            , stateTabWidth        = mkPos 4 }
+      case snd (runParser' (URI.parser <* eof) pst) of
+        Left err' ->
+          case replaceEof "end of URI literal" err' of
+            TrivialError pos us es -> do
+              setPosition (NE.head pos)
+              failure us es
+            FancyError pos xs -> do
+              setPosition (NE.head pos)
+              fancyFailure xs
+        Right x -> return x
+
+pTitle :: IParser Text
+pTitle = choice
+  [ p '\"' '\"'
+  , p '\'' '\''
+  , p '('  ')' ]
+  where
+    p start end = between (char start) (char end) $
+      manyEscapedWith (/= end) "unescaped character"
+
+pAutolink :: IParser Inline
+pAutolink = do
+  notFollowedBy (char '>') -- empty links don't make sense
+  uri <- URI.parser
+  put OtherChar
+  return $ case isEmailUri uri of
+    Nothing ->
+      let txt = (nes . Plain . URI.render) uri
+      in Link txt uri Nothing
+    Just email ->
+      let txt  = nes (Plain email)
+          uri' = URI.makeAbsolute mailtoScheme uri
+      in Link txt uri' Nothing
+
+pEnclosedInline :: IParser Inline
+pEnclosedInline = do
+  let noEmpty = def { iconfigAllowEmpty = False }
+  st <- choice
+    [ pLfdr (DoubleFrame StrongFrame StrongFrame)
+    , pLfdr (DoubleFrame StrongFrame EmphasisFrame)
+    , pLfdr (SingleFrame StrongFrame)
+    , pLfdr (SingleFrame EmphasisFrame)
+    , pLfdr (DoubleFrame StrongFrame_ StrongFrame_)
+    , pLfdr (DoubleFrame StrongFrame_ EmphasisFrame_)
+    , pLfdr (SingleFrame StrongFrame_)
+    , pLfdr (SingleFrame EmphasisFrame_)
+    , pLfdr (DoubleFrame StrikeoutFrame StrikeoutFrame)
+    , pLfdr (DoubleFrame StrikeoutFrame SubscriptFrame)
+    , pLfdr (SingleFrame StrikeoutFrame)
+    , pLfdr (SingleFrame SubscriptFrame)
+    , pLfdr (SingleFrame SuperscriptFrame) ]
+  case st of
+    SingleFrame x ->
+      liftFrame x <$> pInlines noEmpty <* pRfdr x
+    DoubleFrame x y -> do
+      inlines0  <- pInlines noEmpty
+      thisFrame <- pRfdr x <|> pRfdr y
+      let thatFrame = if x == thisFrame then y else x
+      immediate <- True <$ pRfdr thatFrame <|> pure False
+      if immediate
+        then (return . liftFrame thatFrame . nes . liftFrame thisFrame) inlines0
+        else do
+          inlines1 <- pInlines noEmpty
+          void (pRfdr thatFrame)
+          return . liftFrame thatFrame $
+            liftFrame thisFrame inlines0 <| inlines1
+
+pLfdr :: InlineState -> IParser InlineState
+pLfdr st = try $ do
+  let dels = inlineStateDel st
+  mpos <- getNextTokenPosition
+  void (string dels)
+  leftChar   <- get
+  mrightChar <- lookAhead (optional anyChar)
+  let failNow = do
+        forM_ mpos setPosition
+        (mmarkErr . NonFlankingDelimiterRun . toNesTokens) dels
+  case (leftChar, isTransparent <$> mrightChar) of
+    (_, Nothing)          -> failNow
+    (_, Just True)        -> failNow
+    (RightFlankingDel, _) -> failNow
+    (OtherChar, _)        -> failNow
+    (SpaceChar, _)        -> return ()
+    (LeftFlankingDel, _)  -> return ()
+  put LeftFlankingDel
+  return st
+
+pRfdr :: InlineFrame -> IParser InlineFrame
+pRfdr frame = try $ do
+  let dels = inlineFrameDel frame
+  mpos <- getNextTokenPosition
+  void (string dels)
+  leftChar   <- get
+  mrightChar <- lookAhead (optional anyChar)
+  let failNow = do
+        forM_ mpos setPosition
+        (mmarkErr . NonFlankingDelimiterRun . toNesTokens) dels
+  case (leftChar, mrightChar) of
+    (SpaceChar, _) -> failNow
+    (LeftFlankingDel, _) -> failNow
+    (_, Nothing) -> return ()
+    (_, Just rightChar) ->
+      if | isTransparent rightChar -> return ()
+         | isMarkupChar  rightChar -> return ()
+         | otherwise               -> failNow
+  put RightFlankingDel
+  return frame
+
+pHardLineBreak :: IParser Inline
+pHardLineBreak = do
+  void (char '\\')
+  eol
+  notFollowedBy eof
+  sc'
+  put SpaceChar
+  return LineBreak
+
+pPlain :: IParser Inline
+pPlain = Plain . T.pack <$> some
+  (pEscapedChar <|> pNewline <|> pNonEscapedChar)
+  where
+    pEscapedChar = escapedChar <* put OtherChar
+    pNewline = hidden . try $
+      '\n' <$ sc' <* eol <* sc' <* put SpaceChar
+    pNonEscapedChar = label "unescaped non-markup character" . choice $
+      [ try (char '\\' <* notFollowedBy eol)        <* put OtherChar
+      , try (char '!'  <* notFollowedBy (char '[')) <* put SpaceChar
+      , try (char '<'  <* notFollowedBy (pAutolink <* char '>')) <* put OtherChar
+      , spaceChar                                   <* put SpaceChar
+      , satisfy isTrans                             <* put SpaceChar
+      , satisfy isOther                             <* put OtherChar ]
+    isTrans x = isTransparentPunctuation x && x /= '!'
+    isOther x = not (isMarkupChar x) && x /= '\\' && x /= '!' && x /= '<'
+
+----------------------------------------------------------------------------
+-- Parsing helpers
+
+casualLevel :: Parser Pos
+casualLevel = L.indentGuard sc LT (mkPos 5)
+
+casualLevel' :: Parser Pos
+casualLevel' = L.indentGuard sc' LT (mkPos 5)
+
+codeBlockLevel :: Parser Pos
+codeBlockLevel = L.indentGuard sc GT (mkPos 4)
+
+codeBlockLevel' :: Parser Pos
+codeBlockLevel' = L.indentGuard sc' GT (mkPos 4)
+
+nonEmptyLine :: Parser Text
+nonEmptyLine = takeWhile1P Nothing notNewline
+
+manyEscapedWith :: MonadParsec e Text m => (Char -> Bool) -> String -> m Text
+manyEscapedWith f l = T.pack <$> many (escapedChar <|> (satisfy f <?> l))
+
+someEscapedWith :: MonadParsec e Text m => (Char -> Bool) -> m Text
+someEscapedWith f = T.pack <$> some (escapedChar <|> satisfy f)
+
+escapedChar :: MonadParsec e Text m => m Char
+escapedChar = try (char '\\' *> satisfy isAsciiPunctuation)
+  <?> "escaped character"
+
+sc :: MonadParsec e Text m => m ()
+sc = void $ takeWhileP (Just "white space") isSpaceN
+
+sc1 :: MonadParsec e Text m => m ()
+sc1 = void $ takeWhile1P (Just "white space") isSpaceN
+
+sc' :: MonadParsec e Text m => m ()
+sc' = void $ takeWhileP (Just "white space") isSpace
+
+sc1' :: MonadParsec e Text m => m ()
+sc1' = void $ takeWhile1P (Just "white space") isSpace
+
+eol :: MonadParsec e Text m => m ()
+eol = void . label "newline" $ choice
+  [ string "\n"
+  , string "\r\n"
+  , string "\r" ]
+
+eol' :: MonadParsec e Text m => m Bool
+eol' = option False (True <$ eol)
+
+----------------------------------------------------------------------------
+-- Block-level predicates
+
+isThematicBreak :: Text -> Bool
+isThematicBreak l' = T.length l >= 3 && indentLevel l' < 4 &&
+  (T.all (== '*') l ||
+   T.all (== '-') l ||
+   T.all (== '_') l)
+  where
+    l = T.filter (not . isSpace) l'
+
+----------------------------------------------------------------------------
+-- Other helpers
+
+isSpace :: Char -> Bool
+isSpace x = x == ' ' || x == '\t'
+
+isSpaceN :: Char -> Bool
+isSpaceN x = isSpace x || x == '\n' || x == '\r'
+
+notNewline :: Char -> Bool
+notNewline x = x /= '\n' && x /= '\r'
+
+isBlank :: Text -> Bool
+isBlank = T.all isSpace
+
+isMarkupChar :: Char -> Bool
+isMarkupChar = \case
+  '*' -> True
+  '~' -> True
+  '_' -> True
+  '`' -> True
+  '^' -> True
+  '[' -> True
+  ']' -> True
+  _   -> False
+
+isAsciiPunctuation :: Char -> Bool
+isAsciiPunctuation x =
+  (x >= '!' && x <= '/') ||
+  (x >= ':' && x <= '@') ||
+  (x >= '[' && x <= '`') ||
+  (x >= '{' && x <= '~')
+
+isTransparentPunctuation :: Char -> Bool
+isTransparentPunctuation = \case
+  '!' -> True
+  '"' -> True
+  '(' -> True
+  ')' -> True
+  ',' -> True
+  '-' -> True
+  '.' -> True
+  ':' -> True
+  ';' -> True
+  '?' -> True
+  '{' -> True
+  '}' -> True
+  '–' -> True
+  '—' -> True
+  _   -> False
+
+isTransparent :: Char -> Bool
+isTransparent x = Char.isSpace x || isTransparentPunctuation x
+
+nes :: a -> NonEmpty a
+nes a = a :| []
+
+assembleParagraph :: [Text] -> Text
+assembleParagraph = go
+  where
+    go []     = ""
+    go [x]    = T.dropWhileEnd isSpace x
+    go (x:xs) = x <> "\n" <> go xs
+
+assembleCodeBlock :: Pos -> [Text] -> Text
+assembleCodeBlock indent ls = T.unlines (stripIndent indent <$> ls)
+
+indentLevel :: Text -> Int
+indentLevel = T.foldl' f 0 . T.takeWhile isSpace
+  where
+    f n ch
+      | ch == ' '  = n + 1
+      | ch == '\t' = n + 4
+      | otherwise  = n
+
+stripIndent :: Pos -> Text -> Text
+stripIndent indent txt = T.drop m txt
+  where
+    m = snd $ T.foldl' f (0, 0) (T.takeWhile isSpace txt)
+    f (!j, !n) ch
+      | j  >= i    = (j, n)
+      | ch == ' '  = (j + 1, n + 1)
+      | ch == '\t' = (j + 4, n + 1)
+      | otherwise  = (j, n)
+    i = unPos indent - 1
+
+collapseWhiteSpace :: Text -> Text
+collapseWhiteSpace =
+  T.stripEnd . T.filter (/= '\0') . snd . T.mapAccumL f True
+  where
+    f seenSpace ch =
+      case (seenSpace, g ch) of
+        (False, False) -> (False, ch)
+        (True,  False) -> (False, ch)
+        (False, True)  -> (True,  ' ')
+        (True,  True)  -> (True,  '\0')
+    g ' '  = True
+    g '\t' = True
+    g '\n' = True
+    g _    = False
+
+inlineFrameDel :: InlineFrame -> Text
+inlineFrameDel = \case
+  EmphasisFrame    -> "*"
+  EmphasisFrame_   -> "_"
+  StrongFrame      -> "**"
+  StrongFrame_     -> "__"
+  StrikeoutFrame   -> "~~"
+  SubscriptFrame   -> "~"
+  SuperscriptFrame -> "^"
+
+inlineStateDel :: InlineState -> Text
+inlineStateDel = \case
+  SingleFrame x   -> inlineFrameDel x
+  DoubleFrame x y -> inlineFrameDel x <> inlineFrameDel y
+
+liftFrame :: InlineFrame -> NonEmpty Inline -> Inline
+liftFrame = \case
+  StrongFrame      -> Strong
+  EmphasisFrame    -> Emphasis
+  StrongFrame_     -> Strong
+  EmphasisFrame_   -> Emphasis
+  StrikeoutFrame   -> Strikeout
+  SubscriptFrame   -> Subscript
+  SuperscriptFrame -> Superscript
+
+replaceEof :: String -> ParseError Char e -> ParseError Char e
+replaceEof altLabel = \case
+  TrivialError pos us es -> TrivialError pos (f <$> us) (E.map f es)
+  FancyError   pos xs    -> FancyError pos xs
+  where
+    f EndOfInput = Label (NE.fromList altLabel)
+    f x          = x
+
+mmarkErr :: MonadParsec MMarkErr s m => MMarkErr -> m a
+mmarkErr = fancyFailure . E.singleton . ErrorCustom
+
+toNesTokens :: Text -> NonEmpty Char
+toNesTokens = NE.fromList . T.unpack
+
+isEmailUri :: URI -> Maybe Text
+isEmailUri uri =
+  case URI.unRText <$> URI.uriPath uri of
+    [x] ->
+      if Email.isValid (TE.encodeUtf8 x) &&
+          (isNothing (URI.uriScheme uri) ||
+           URI.uriScheme uri == Just mailtoScheme)
+        then Just x
+        else Nothing
+    _ -> Nothing
+
+mailtoScheme :: URI.RText 'URI.Scheme
+mailtoScheme = fromJust (URI.mkScheme "mailto")
+
+splitYamlError :: FilePath -> String -> (Maybe SourcePos, String)
+splitYamlError file str = maybe (Nothing, str) (first pure) (parseMaybe p str)
+  where
+    p :: Parsec Void String (SourcePos, String)
+    p = do
+      void (string "YAML parse exception at line ")
+      l <- mkPos . (+ 2) <$> L.decimal
+      void (string ", column ")
+      c <- mkPos . (+ 1) <$> L.decimal
+      void (string ":\n")
+      r <- takeRest
+      return (SourcePos file l c, r)
diff --git a/bench/memory/Main.hs b/bench/memory/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/memory/Main.hs
@@ -0,0 +1,27 @@
+module Main (main) where
+
+import Weigh
+import qualified Data.Text.IO as T
+import qualified Text.MMark   as MMark
+
+main :: IO ()
+main = mainWith $ do
+  setColumns [Case, Allocated, GCs, Max]
+  bparser "data/bench-yaml-block.md"
+  bparser "data/bench-thematic-break.md"
+  bparser "data/bench-heading.md"
+  bparser "data/bench-fenced-code-block.md"
+  bparser "data/bench-indented-code-block.md"
+  bparser "data/bench-paragraph.md"
+  bparser "data/comprehensive.md"
+
+----------------------------------------------------------------------------
+-- Helpers
+
+bparser
+  :: FilePath          -- ^ File from which the input has been loaded
+  -> Weigh ()
+bparser path = action name (p <$> T.readFile path)
+  where
+    name = "with file: " ++ path
+    p    = MMark.parse path
diff --git a/bench/speed/Main.hs b/bench/speed/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/speed/Main.hs
@@ -0,0 +1,27 @@
+module Main (main) where
+
+import Criterion.Main
+import qualified Data.Text.IO as T
+import qualified Text.MMark   as MMark
+
+main :: IO ()
+main = defaultMain
+  [ bparser "data/bench-yaml-block.md"
+  , bparser "data/bench-thematic-break.md"
+  , bparser "data/bench-heading.md"
+  , bparser "data/bench-fenced-code-block.md"
+  , bparser "data/bench-indented-code-block.md"
+  , bparser "data/bench-paragraph.md"
+  , bparser "data/comprehensive.md"
+  ]
+
+----------------------------------------------------------------------------
+-- Helpers
+
+bparser
+  :: FilePath          -- ^ File from which to load parser's input
+  -> Benchmark
+bparser path = env (T.readFile path) (bench name . nf p)
+  where
+    name = "with file: " ++ path
+    p    = MMark.parse path
diff --git a/data/bench-fenced-code-block.md b/data/bench-fenced-code-block.md
new file mode 100644
--- /dev/null
+++ b/data/bench-fenced-code-block.md
@@ -0,0 +1,12 @@
+```
+Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat
+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi
+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent
+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut
+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et
+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum
+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet
+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.
+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae
+massa luctus malesuada a vel diam.
+```
diff --git a/data/bench-heading.md b/data/bench-heading.md
new file mode 100644
--- /dev/null
+++ b/data/bench-heading.md
@@ -0,0 +1,6 @@
+# Heading 1 blah blah blah
+## Heading 2 blah blah blah
+### Heading 3 blah blah blah
+#### Heading 4 blah blah blah
+##### Heading 5 blah blah blah
+###### Heading 6 blah blah blah
diff --git a/data/bench-indented-code-block.md b/data/bench-indented-code-block.md
new file mode 100644
--- /dev/null
+++ b/data/bench-indented-code-block.md
@@ -0,0 +1,10 @@
+    Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat
+    massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi
+    posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent
+    egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut
+    ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et
+    magnis dis parturient montes, nascetur ridiculus mus. Integer interdum
+    malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet
+    sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.
+    Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae
+    massa luctus malesuada a vel diam.
diff --git a/data/bench-paragraph.md b/data/bench-paragraph.md
new file mode 100644
--- /dev/null
+++ b/data/bench-paragraph.md
@@ -0,0 +1,10 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi
+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.
+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec
+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem
+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor
+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula
+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.
+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam
+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum
+varius ex in mollis.
diff --git a/data/bench-thematic-break.md b/data/bench-thematic-break.md
new file mode 100644
--- /dev/null
+++ b/data/bench-thematic-break.md
@@ -0,0 +1,6 @@
+***
+---
+__**
+ - - -
+ **  * ** * ** * **
+-     -     -     -
diff --git a/data/bench-yaml-block.md b/data/bench-yaml-block.md
new file mode 100644
--- /dev/null
+++ b/data/bench-yaml-block.md
@@ -0,0 +1,7 @@
+---
+title: Free monad considered harmful
+desc: Before you start writing your code using free monads read this, you may change your mind.
+date:
+  published: September 27, 2017
+  updated: September 29, 2017
+---
diff --git a/data/comprehensive.html b/data/comprehensive.html
new file mode 100644
--- /dev/null
+++ b/data/comprehensive.html
@@ -0,0 +1,139 @@
+<h1 id="lorem-ipsum">Lorem ipsum</h1>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi
+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.
+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec
+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem
+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor
+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula
+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.
+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam
+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum
+varius ex in mollis.</p>
+<p><a href="https://example.org/">https://example.org/</a></p>
+<p>Ut in imperdiet neque. Etiam iaculis rhoncus nisl vel porta. Praesent velit
+orci, laoreet suscipit bibendum eu, ornare et orci. Fusce feugiat, felis a
+vehicula pulvinar, nulla purus dictum arcu, et varius urna purus et nibh.
+Duis lobortis fringilla ligula, in aliquet sem maximus a. Suspendisse
+potenti. Nullam consequat tellus a lectus vestibulum faucibus. Ut hendrerit
+dolor ut libero efficitur accumsan. Mauris dapibus, leo non porttitor
+lobortis, lectus ipsum tempor metus, quis iaculis arcu quam malesuada nulla.</p>
+<h2 id="nullam-luctus">Nullam luctus</h2>
+<p>Nullam <a href="http://example.org/luctus">luctus</a> placerat nisl in dapibus.
+Phasellus id erat eros. Ut gravida risus sit amet massa tempor volutpat.
+Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
+cubilia Curae; Quisque dictum sapien vel enim tempor, quis ornare justo
+consequat. Suspendisse porttitor mollis consectetur. Curabitur sodales,
+risus eu dapibus mattis, tellus dolor condimentum dolor, in ultricies nibh
+augue vel nibh. Vivamus imperdiet, orci id posuere sollicitudin, diam purus
+consequat eros, quis dictum mauris lacus ac lectus. Cras vitae pharetra
+risus. Maecenas vehicula, leo vitae semper tristique, libero urna
+consectetur massa, eget pharetra magna est nec massa. Vestibulum malesuada
+lobortis lacinia.</p>
+<p><img src="https://example.org/image.png" alt="My image"></p>
+<p>Phasellus tincidunt metus quam, vel mollis turpis ultrices et. Phasellus
+consequat diam eu turpis sollicitudin tempus. Fusce suscipit bibendum nisl,
+quis rutrum eros volutpat in. Fusce tempor nisi eu ligula volutpat, eu
+ultricies arcu blandit. Pellentesque habitant morbi tristique senectus et
+netus et malesuada fames ac turpis egestas. Duis eleifend malesuada
+venenatis. Morbi tincidunt quis diam ac aliquam.</p>
+<h2 id="sed-euismod">Sed euismod</h2>
+<p>Sed euismod nisi lorem, ac tempor nibh venenatis at. Integer porta nibh quis
+mauris vehicula porta. Sed vel tellus nec lacus porttitor sollicitudin. Sed
+facilisis nisl lorem, sed aliquet leo convallis sed. Curabitur vitae aliquet
+diam, ac commodo ligula. Nulla aliquet odio at tellus auctor pellentesque.
+In sagittis elementum tortor sed lobortis. Fusce nibh turpis, posuere eget
+tristique eget, commodo quis leo.</p>
+<hr>
+<p>Etiam faucibus, ipsum id lobortis molestie, dolor lectus cursus purus, nec
+volutpat massa odio vitae ligula. Nulla non consectetur ligula. In sem
+felis, vehicula a convallis ut, pellentesque nec diam. Integer ullamcorper
+rutrum nulla. Nam arcu dolor, placerat nec molestie et, eleifend sit amet
+ante. Suspendisse laoreet orci sit amet vestibulum varius. In at leo eu
+lorem tincidunt facilisis. Ut elementum elit ornare risus convallis, ut
+viverra orci pretium. Vivamus mi orci, lacinia ac ligula a, condimentum
+aliquet ligula.</p>
+<h3 id="curabitur-ullamcorper">Curabitur ullamcorper</h3>
+<p>Curabitur ullamcorper, lectus id porttitor vehicula, augue purus ornare
+orci, ut consequat tellus mauris ac sem. Cras tincidunt sagittis mi, sit
+amet viverra erat ultrices vulputate. Donec urna nulla, malesuada non cursus
+et, posuere eu sapien. Fusce cursus mauris odio, id tincidunt felis
+tincidunt sed. Duis vulputate lectus eu tellus pretium gravida. Nunc at eros
+fringilla mi egestas imperdiet. In bibendum justo sapien, sed commodo tellus
+auctor sit amet. Fusce at purus turpis. Aliquam a nibh at massa hendrerit
+mollis a nec ipsum. Sed porta erat vitae justo sodales gravida nec sed
+augue. Maecenas ultrices tristique hendrerit.</p>
+<p>Curabitur venenatis vestibulum quam, a facilisis odio dignissim in.
+Vestibulum ut turpis pharetra, aliquam metus a, dapibus massa. Mauris
+vehicula sapien quis dolor facilisis, in placerat ipsum accumsan. Morbi
+accumsan accumsan velit, vel auctor mauris suscipit non. Proin ex lacus,
+dapibus et leo eu, lacinia viverra metus. Suspendisse potenti. Maecenas
+finibus justo lectus, nec commodo velit cursus sit amet. In at lacus vel
+augue porta volutpat eleifend ut orci.</p>
+<h2 id="ut-dictum-quis">Ut dictum quis</h2>
+<p>Ut dictum quis felis quis auctor. Phasellus lacinia tellus quis massa
+commodo dignissim. Nam vitae felis non ex auctor tincidunt sit amet et
+lorem. Nulla bibendum odio suscipit, tincidunt felis eget, mollis tortor.
+Aliquam a feugiat augue, sed posuere massa. Donec pellentesque sodales
+varius. Suspendisse quis mollis massa. Morbi imperdiet at felis in feugiat.
+Praesent mauris urna, suscipit vitae condimentum venenatis, fermentum sit
+amet purus. Cras non placerat nulla, vel lobortis ex. Nunc ut pulvinar
+magna. Nulla vitae arcu turpis. Suspendisse elementum odio eros. Nullam
+blandit molestie nibh eget pulvinar. Quisque placerat ante nec pulvinar
+fringilla.</p>
+<pre><code>Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat
+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi
+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent
+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut
+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et
+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum
+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet
+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.
+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae
+massa luctus malesuada a vel diam.
+</code></pre>
+<p>Proin sit amet erat consequat, pellentesque augue at, efficitur libero. Nunc
+pretium nulla eros, vitae euismod ex commodo sit amet. Pellentesque laoreet
+ac enim id commodo. Integer eleifend nibh quis lorem pellentesque venenatis.
+Nulla et eros sit amet diam pharetra porttitor. Sed ullamcorper rutrum
+volutpat. Quisque accumsan posuere arcu, scelerisque dignissim ipsum. Donec
+congue odio ultricies eros maximus, id maximus metus bibendum. Aenean ornare
+metus non nisi varius venenatis. Vestibulum ornare id urna quis tempus. Cras
+non arcu vitae magna feugiat sodales. Fusce eget posuere ligula. Donec
+porttitor, odio at scelerisque tempus, nunc enim cursus dui, finibus
+tincidunt justo lorem quis urna. Integer mauris quam, ultrices quis enim ut,
+consectetur consequat sapien. In dapibus arcu eget ultrices vehicula.</p>
+<h3 id="mauris-efficitur">Mauris efficitur</h3>
+<p>Mauris efficitur mollis purus, nec porttitor ligula condimentum a. Proin
+ultricies semper neque ac pulvinar. Nullam vestibulum leo justo, eget
+vehicula elit lobortis vitae. Suspendisse commodo nunc et lorem molestie
+lobortis. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Fusce nec mi ultricies, sagittis neque
+sed, tincidunt leo. Proin in lorem a libero maximus posuere. Pellentesque
+habitant morbi tristique senectus et netus et malesuada fames ac turpis
+egestas. Sed interdum eget ipsum id ullamcorper.</p>
+<p>Aenean feugiat orci leo. Morbi fringilla, tortor id posuere mollis, lectus
+est ullamcorper tellus, nec sagittis augue nibh nec est. Sed pulvinar orci a
+justo eleifend dapibus. Phasellus aliquam enim in semper tincidunt. Donec
+tempor tristique purus eu pretium. In ornare est at varius elementum. Cras
+finibus nisl in nisi vestibulum, sed sollicitudin eros finibus.</p>
+<p>Nullam sed nisi blandit, ultrices neque a, finibus ligula. Etiam a ante
+sagittis nibh ornare commodo. Morbi id elit vehicula, gravida enim vitae,
+suscipit orci. Nunc mattis enim sit amet sem dictum molestie. Nam semper
+arcu a libero molestie, at accumsan turpis egestas. Quisque at pulvinar
+enim. Cras porttitor, dolor et commodo accumsan, nibh sem dapibus velit, sit
+amet maximus urna justo id eros. Praesent vitae aliquet lorem, in facilisis
+nulla. Proin sit amet odio nisl. Phasellus sed felis velit. Maecenas pretium
+posuere laoreet. Nullam dapibus ullamcorper sapien. Integer nec quam elit.
+Fusce at dictum lacus. Nam risus dui, efficitur eget urna at, ultricies
+sagittis ligula.</p>
+<h2 id="aenean-auctor-nec">Aenean auctor nec</h2>
+<p>Aenean auctor nec libero ut laoreet. Etiam vulputate lorem quis ex
+dignissim, sit amet tincidunt ligula scelerisque. Nam venenatis blandit
+ipsum quis maximus. Curabitur vitae dolor a diam viverra efficitur. Nulla
+feugiat iaculis orci quis sodales. Aliquam consequat bibendum turpis, quis
+scelerisque lorem fermentum feugiat. Proin pellentesque interdum odio ac
+ullamcorper. Proin laoreet purus leo, non eleifend velit venenatis id.
+Aliquam consectetur finibus turpis quis sollicitudin. Duis congue est nec
+odio hendrerit scelerisque at at sapien. Nunc lacinia ac leo sit amet
+sodales. Donec posuere fermentum massa, at tristique nulla aliquet nec.
+Mauris dapibus quam non augue maximus suscipit.</p>
diff --git a/data/comprehensive.md b/data/comprehensive.md
new file mode 100644
--- /dev/null
+++ b/data/comprehensive.md
@@ -0,0 +1,164 @@
+# Lorem ipsum
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi
+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.
+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec
+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem
+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor
+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula
+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.
+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam
+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum
+varius ex in mollis.
+
+<https://example.org>
+
+Ut in imperdiet neque. Etiam iaculis rhoncus nisl vel porta. Praesent velit
+orci, laoreet suscipit bibendum eu, ornare et orci. Fusce feugiat, felis a
+vehicula pulvinar, nulla purus dictum arcu, et varius urna purus et nibh.
+Duis lobortis fringilla ligula, in aliquet sem maximus a. Suspendisse
+potenti. Nullam consequat tellus a lectus vestibulum faucibus. Ut hendrerit
+dolor ut libero efficitur accumsan. Mauris dapibus, leo non porttitor
+lobortis, lectus ipsum tempor metus, quis iaculis arcu quam malesuada nulla.
+
+## Nullam luctus
+
+Nullam [luctus](http://example.org/luctus) placerat nisl in dapibus.
+Phasellus id erat eros. Ut gravida risus sit amet massa tempor volutpat.
+Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
+cubilia Curae; Quisque dictum sapien vel enim tempor, quis ornare justo
+consequat. Suspendisse porttitor mollis consectetur. Curabitur sodales,
+risus eu dapibus mattis, tellus dolor condimentum dolor, in ultricies nibh
+augue vel nibh. Vivamus imperdiet, orci id posuere sollicitudin, diam purus
+consequat eros, quis dictum mauris lacus ac lectus. Cras vitae pharetra
+risus. Maecenas vehicula, leo vitae semper tristique, libero urna
+consectetur massa, eget pharetra magna est nec massa. Vestibulum malesuada
+lobortis lacinia.
+
+![My image](https://example.org/image.png)
+
+Phasellus tincidunt metus quam, vel mollis turpis ultrices et. Phasellus
+consequat diam eu turpis sollicitudin tempus. Fusce suscipit bibendum nisl,
+quis rutrum eros volutpat in. Fusce tempor nisi eu ligula volutpat, eu
+ultricies arcu blandit. Pellentesque habitant morbi tristique senectus et
+netus et malesuada fames ac turpis egestas. Duis eleifend malesuada
+venenatis. Morbi tincidunt quis diam ac aliquam.
+
+## Sed euismod
+
+Sed euismod nisi lorem, ac tempor nibh venenatis at. Integer porta nibh quis
+mauris vehicula porta. Sed vel tellus nec lacus porttitor sollicitudin. Sed
+facilisis nisl lorem, sed aliquet leo convallis sed. Curabitur vitae aliquet
+diam, ac commodo ligula. Nulla aliquet odio at tellus auctor pellentesque.
+In sagittis elementum tortor sed lobortis. Fusce nibh turpis, posuere eget
+tristique eget, commodo quis leo.
+
+***
+
+Etiam faucibus, ipsum id lobortis molestie, dolor lectus cursus purus, nec
+volutpat massa odio vitae ligula. Nulla non consectetur ligula. In sem
+felis, vehicula a convallis ut, pellentesque nec diam. Integer ullamcorper
+rutrum nulla. Nam arcu dolor, placerat nec molestie et, eleifend sit amet
+ante. Suspendisse laoreet orci sit amet vestibulum varius. In at leo eu
+lorem tincidunt facilisis. Ut elementum elit ornare risus convallis, ut
+viverra orci pretium. Vivamus mi orci, lacinia ac ligula a, condimentum
+aliquet ligula.
+
+### Curabitur ullamcorper
+
+Curabitur ullamcorper, lectus id porttitor vehicula, augue purus ornare
+orci, ut consequat tellus mauris ac sem. Cras tincidunt sagittis mi, sit
+amet viverra erat ultrices vulputate. Donec urna nulla, malesuada non cursus
+et, posuere eu sapien. Fusce cursus mauris odio, id tincidunt felis
+tincidunt sed. Duis vulputate lectus eu tellus pretium gravida. Nunc at eros
+fringilla mi egestas imperdiet. In bibendum justo sapien, sed commodo tellus
+auctor sit amet. Fusce at purus turpis. Aliquam a nibh at massa hendrerit
+mollis a nec ipsum. Sed porta erat vitae justo sodales gravida nec sed
+augue. Maecenas ultrices tristique hendrerit.
+
+Curabitur venenatis vestibulum quam, a facilisis odio dignissim in.
+Vestibulum ut turpis pharetra, aliquam metus a, dapibus massa. Mauris
+vehicula sapien quis dolor facilisis, in placerat ipsum accumsan. Morbi
+accumsan accumsan velit, vel auctor mauris suscipit non. Proin ex lacus,
+dapibus et leo eu, lacinia viverra metus. Suspendisse potenti. Maecenas
+finibus justo lectus, nec commodo velit cursus sit amet. In at lacus vel
+augue porta volutpat eleifend ut orci.
+
+## Ut dictum quis
+
+Ut dictum quis felis quis auctor. Phasellus lacinia tellus quis massa
+commodo dignissim. Nam vitae felis non ex auctor tincidunt sit amet et
+lorem. Nulla bibendum odio suscipit, tincidunt felis eget, mollis tortor.
+Aliquam a feugiat augue, sed posuere massa. Donec pellentesque sodales
+varius. Suspendisse quis mollis massa. Morbi imperdiet at felis in feugiat.
+Praesent mauris urna, suscipit vitae condimentum venenatis, fermentum sit
+amet purus. Cras non placerat nulla, vel lobortis ex. Nunc ut pulvinar
+magna. Nulla vitae arcu turpis. Suspendisse elementum odio eros. Nullam
+blandit molestie nibh eget pulvinar. Quisque placerat ante nec pulvinar
+fringilla.
+
+```
+Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat
+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi
+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent
+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut
+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et
+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum
+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet
+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.
+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae
+massa luctus malesuada a vel diam.
+```
+
+Proin sit amet erat consequat, pellentesque augue at, efficitur libero. Nunc
+pretium nulla eros, vitae euismod ex commodo sit amet. Pellentesque laoreet
+ac enim id commodo. Integer eleifend nibh quis lorem pellentesque venenatis.
+Nulla et eros sit amet diam pharetra porttitor. Sed ullamcorper rutrum
+volutpat. Quisque accumsan posuere arcu, scelerisque dignissim ipsum. Donec
+congue odio ultricies eros maximus, id maximus metus bibendum. Aenean ornare
+metus non nisi varius venenatis. Vestibulum ornare id urna quis tempus. Cras
+non arcu vitae magna feugiat sodales. Fusce eget posuere ligula. Donec
+porttitor, odio at scelerisque tempus, nunc enim cursus dui, finibus
+tincidunt justo lorem quis urna. Integer mauris quam, ultrices quis enim ut,
+consectetur consequat sapien. In dapibus arcu eget ultrices vehicula.
+
+### Mauris efficitur
+
+Mauris efficitur mollis purus, nec porttitor ligula condimentum a. Proin
+ultricies semper neque ac pulvinar. Nullam vestibulum leo justo, eget
+vehicula elit lobortis vitae. Suspendisse commodo nunc et lorem molestie
+lobortis. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Fusce nec mi ultricies, sagittis neque
+sed, tincidunt leo. Proin in lorem a libero maximus posuere. Pellentesque
+habitant morbi tristique senectus et netus et malesuada fames ac turpis
+egestas. Sed interdum eget ipsum id ullamcorper.
+
+Aenean feugiat orci leo. Morbi fringilla, tortor id posuere mollis, lectus
+est ullamcorper tellus, nec sagittis augue nibh nec est. Sed pulvinar orci a
+justo eleifend dapibus. Phasellus aliquam enim in semper tincidunt. Donec
+tempor tristique purus eu pretium. In ornare est at varius elementum. Cras
+finibus nisl in nisi vestibulum, sed sollicitudin eros finibus.
+
+Nullam sed nisi blandit, ultrices neque a, finibus ligula. Etiam a ante
+sagittis nibh ornare commodo. Morbi id elit vehicula, gravida enim vitae,
+suscipit orci. Nunc mattis enim sit amet sem dictum molestie. Nam semper
+arcu a libero molestie, at accumsan turpis egestas. Quisque at pulvinar
+enim. Cras porttitor, dolor et commodo accumsan, nibh sem dapibus velit, sit
+amet maximus urna justo id eros. Praesent vitae aliquet lorem, in facilisis
+nulla. Proin sit amet odio nisl. Phasellus sed felis velit. Maecenas pretium
+posuere laoreet. Nullam dapibus ullamcorper sapien. Integer nec quam elit.
+Fusce at dictum lacus. Nam risus dui, efficitur eget urna at, ultricies
+sagittis ligula.
+
+## Aenean auctor nec
+
+Aenean auctor nec libero ut laoreet. Etiam vulputate lorem quis ex
+dignissim, sit amet tincidunt ligula scelerisque. Nam venenatis blandit
+ipsum quis maximus. Curabitur vitae dolor a diam viverra efficitur. Nulla
+feugiat iaculis orci quis sodales. Aliquam consequat bibendum turpis, quis
+scelerisque lorem fermentum feugiat. Proin pellentesque interdum odio ac
+ullamcorper. Proin laoreet purus leo, non eleifend velit venenatis id.
+Aliquam consectetur finibus turpis quis sollicitudin. Duis congue est nec
+odio hendrerit scelerisque at at sapien. Nunc lacinia ac leo sit amet
+sodales. Donec posuere fermentum massa, at tristique nulla aliquet nec.
+Mauris dapibus quam non augue maximus suscipit.
diff --git a/mmark.cabal b/mmark.cabal
new file mode 100644
--- /dev/null
+++ b/mmark.cabal
@@ -0,0 +1,112 @@
+name:                 mmark
+version:              0.0.1.0
+cabal-version:        >= 1.18
+tested-with:          GHC==7.10.3, GHC==8.0.2, GHC==8.2.1
+license:              BSD3
+license-file:         LICENSE.md
+author:               Mark Karpov <markkarpov92@gmail.com>
+maintainer:           Mark Karpov <markkarpov92@gmail.com>
+homepage:             https://github.com/mrkkrp/mmark
+bug-reports:          https://github.com/mrkkrp/mmark/issues
+category:             Text
+synopsis:             Strict markdown processor for writers
+build-type:           Simple
+description:          Strict markdown processor for writers.
+extra-doc-files:      CHANGELOG.md
+                    , README.md
+data-files:           data/*.md
+                    , data/*.html
+
+source-repository head
+  type:               git
+  location:           https://github.com/mrkkrp/mmark.git
+
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
+
+library
+  build-depends:      aeson            >= 0.11 && < 1.3
+                    , base             >= 4.8  && < 5.0
+                    , containers       >= 0.5  && < 0.6
+                    , data-default-class
+                    , deepseq          >= 1.3  && < 1.5
+                    , email-validate   >= 2.2  && < 2.4
+                    , foldl            >= 1.2  && < 1.4
+                    , lucid            >= 2.6  && < 3.0
+                    , megaparsec       >= 6.1  && < 7.0
+                    , modern-uri       >= 0.1.1 && < 0.2
+                    , mtl              >= 2.0  && < 3.0
+                    , parser-combinators >= 0.2 && < 1.0
+                    , text             >= 0.2  && < 1.3
+                    , yaml             >= 0.8.10 && < 0.9
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups       == 0.18.*
+  if !impl(ghc >= 7.10)
+    build-depends:    void             == 0.7.*
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups       == 0.18.*
+  exposed-modules:    Text.MMark
+                    , Text.MMark.Extension
+  other-modules:      Text.MMark.Internal
+                    , Text.MMark.Parser
+  if flag(dev)
+    ghc-options:      -O0 -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
+
+test-suite tests
+  main-is:            Spec.hs
+  hs-source-dirs:     tests
+  type:               exitcode-stdio-1.0
+  build-depends:      QuickCheck       >= 2.4  && < 3.0
+                    , aeson            >= 0.11 && < 1.3
+                    , base             >= 4.8  && < 5.0
+                    , foldl            >= 1.2  && < 1.4
+                    , hspec            >= 2.0  && < 3.0
+                    , hspec-megaparsec >= 1.0  && < 2.0
+                    , lucid            >= 2.6  && < 3.0
+                    , megaparsec       >= 6.1  && < 7.0
+                    , mmark
+                    , modern-uri       >= 0.1.1 && < 0.2
+                    , text             >= 0.2  && < 1.3
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups       == 0.18.*
+  other-modules:      Text.MMarkSpec
+                    , Text.MMark.ExtensionSpec
+                    , Text.MMark.TestUtils
+  if flag(dev)
+    ghc-options:      -O0 -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
+
+benchmark bench-speed
+  main-is:            Main.hs
+  hs-source-dirs:     bench/speed
+  type:               exitcode-stdio-1.0
+  build-depends:      base             >= 4.8 && < 5.0
+                    , criterion        >= 0.6.2.1 && < 1.3
+                    , mmark
+                    , text             >= 0.2 && < 1.3
+  if flag(dev)
+    ghc-options:      -O2 -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
+
+benchmark bench-memory
+  main-is:            Main.hs
+  hs-source-dirs:     bench/memory
+  type:               exitcode-stdio-1.0
+  build-depends:      base             >= 4.8 && < 5.0
+                    , mmark
+                    , text             >= 0.2 && < 1.3
+                    , weigh            >= 0.0.4
+  if flag(dev)
+    ghc-options:      -O2 -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/tests/Text/MMark/ExtensionSpec.hs b/tests/Text/MMark/ExtensionSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/MMark/ExtensionSpec.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE LambdaCase           #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Text.MMark.ExtensionSpec (spec) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Text (Text)
+import Test.Hspec
+import Test.QuickCheck
+import Text.MMark.Extension (Block (..), Inline (..))
+import Text.MMark.TestUtils
+import qualified Data.Text            as T
+import qualified Lucid                as L
+import qualified Text.MMark           as MMark
+import qualified Text.MMark.Extension as Ext
+import qualified Text.URI             as URI
+
+spec :: Spec
+spec = parallel $ do
+  describe "blockTrans" $
+    it "works" $ do
+      doc <- mkDoc "# My heading"
+      toText (MMark.useExtension h1_to_h2 doc)
+        `shouldBe` "<h2 id=\"my-heading\">My heading</h2>\n"
+  describe "blockRender" $
+    it "works" $ do
+      doc <- mkDoc "# My heading"
+      toText (MMark.useExtension add_h1_content doc)
+        `shouldBe` "<h1 data-content=\"My heading\" id=\"my-heading\">My heading</h1>\n"
+  describe "inlineTrans" $
+    it "works" $ do
+      doc <- mkDoc "# My *heading*"
+      toText (MMark.useExtension em_to_strong doc)
+        `shouldBe` "<h1 id=\"my-heading\">My <strong>heading</strong></h1>\n"
+  describe "inlineRender" $
+    it "works" $ do
+      doc <- mkDoc "# My *heading*"
+      toText (MMark.useExtension (add_em_class "foo") doc)
+        `shouldBe` "<h1 id=\"my-heading\">My <em class=\"foo\">heading</em></h1>\n"
+  describe "asPlainText" $ do
+    let f x = Ext.asPlainText (x:|[])
+    context "with Plain" $
+      it "works" $
+        property $ \txt ->
+          f (Plain txt) `shouldBe` txt
+    context "with LineBreak" $
+      it "works" $
+        f LineBreak `shouldBe` "\n"
+    context "with Emphasis" $
+      it "works" $
+        property $ \txt ->
+          f (Emphasis $ Plain txt :| []) `shouldBe` txt
+    context "with Strong" $
+      it "works" $
+        property $ \txt ->
+          f (Strong $ Plain txt :| []) `shouldBe` txt
+    context "with Strikeout" $
+      it "works" $
+        property $ \txt ->
+          f (Strikeout $ Plain txt :| []) `shouldBe` txt
+    context "with Subscript" $
+      it "works" $
+        property $ \txt ->
+          f (Subscript $ Plain txt :| []) `shouldBe` txt
+    context "with Superscript" $
+      it "works" $
+        property $ \txt ->
+          f (Superscript $ Plain txt :| []) `shouldBe` txt
+    context "with CodeSpan" $
+      it "works" $
+        property $ \txt ->
+          f (CodeSpan txt) `shouldBe` txt
+    context "with Link" $
+      it "works" $
+        property $ \txt uri ->
+          f (Link (Plain txt :| []) uri Nothing) `shouldBe` txt
+    context "with Image" $
+      it "works" $
+        property $ \txt uri ->
+          f (Image (Plain txt :| []) uri Nothing) `shouldBe` txt
+  describe "headerId" $
+    it "works" $
+      Ext.headerId (Plain "Something like that":| []) `shouldBe`
+        "something-like-that"
+  describe "headerFragment" $
+    it "generates URIs with just that fragment" $
+      property $ \fragment -> do
+        let uri = Ext.headerFragment fragment
+        frag <- URI.mkFragment fragment
+        URI.uriScheme    uri `shouldBe` Nothing
+        URI.uriAuthority uri `shouldBe` Left False
+        URI.uriPath      uri `shouldBe` []
+        URI.uriQuery     uri `shouldBe` []
+        URI.uriFragment  uri `shouldBe` Just frag
+
+----------------------------------------------------------------------------
+-- Arbitrary instances
+
+instance Arbitrary Text where
+  arbitrary = T.pack <$> arbitrary
+
+----------------------------------------------------------------------------
+-- Testing extensions
+
+-- | Convert H1 headings into H2 headings.
+
+h1_to_h2 :: MMark.Extension
+h1_to_h2 = Ext.blockTrans $ \case
+  Heading1 inner -> Heading2 inner
+  other          -> other
+
+-- | Add a data attribute calculated based on plain text contents of the
+-- level 1 heading to test the 'Ext.getOis' thing and 'Ext.blockRender' in
+-- general.
+
+add_h1_content :: MMark.Extension
+add_h1_content = Ext.blockRender $ \old block ->
+  case block of
+    Heading1 inner -> L.with (old (Heading1 inner))
+      [ L.data_ "content" (Ext.asPlainText . Ext.getOis . fst $ inner) ]
+    other          -> old other
+
+-- | Covert all 'Emphasis' to 'Strong'.
+
+em_to_strong :: MMark.Extension
+em_to_strong = Ext.inlineTrans $ \case
+  Emphasis inner -> Strong inner
+  other          -> other
+
+-- | Add given class to all 'Emphasis' things.
+
+add_em_class :: Text -> MMark.Extension
+add_em_class given = Ext.inlineRender $ \old inline ->
+  case inline of
+    Emphasis inner -> L.with (old (Emphasis inner)) [L.class_ given]
+    other          -> old other
diff --git a/tests/Text/MMark/TestUtils.hs b/tests/Text/MMark/TestUtils.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/MMark/TestUtils.hs
@@ -0,0 +1,111 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.MMark.TestUtils
+  ( -- * Document creation and rendering
+    mkDoc
+  , toText
+    -- * Parser expectations
+  , (~~->)
+  , (~->)
+  , (=->)
+  , (==->) )
+where
+
+import Control.Monad
+import Data.Text (Text)
+import Test.Hspec
+import Text.MMark (MMark, MMarkErr)
+import Text.Megaparsec
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Text          as T
+import qualified Data.Text.Lazy     as TL
+import qualified Lucid              as L
+import qualified Text.MMark         as MMark
+
+----------------------------------------------------------------------------
+-- Document creation and rendering
+
+-- | Create an 'MMark' document from given input reporting an expectation
+-- failure if it cannot be parsed.
+
+mkDoc :: Text -> IO MMark
+mkDoc input =
+  case MMark.parse "" input of
+    Left errs -> do
+      expectationFailure $
+        "while parsing a document, parse error(s) occurred:\n" ++
+        MMark.parseErrorsPretty input errs
+      undefined
+    Right x -> return x
+
+-- | Render an 'MMark' document to 'Text'.
+
+toText :: MMark -> Text
+toText = TL.toStrict . L.renderText . MMark.render
+
+----------------------------------------------------------------------------
+-- Parser expectations
+
+-- | Create an expectation that parser should fail producing a certain
+-- collection of 'ParseError's.
+
+infix 2 ~~->
+
+(~~->)
+  :: Text
+     -- ^ Input for parser
+  -> [ParseError Char MMarkErr]
+     -- ^ Expected collection of parse errors, in order
+  -> Expectation
+input ~~-> errs'' =
+  case MMark.parse "" input of
+    Left errs' -> unless (errs == errs') . expectationFailure $
+      "the parser is expected to fail with:\n" ++
+      MMark.parseErrorsPretty input errs       ++
+      "but it failed with:\n"                  ++
+      MMark.parseErrorsPretty input errs'
+    Right x -> expectationFailure $
+      "the parser is expected to fail, but it parsed: " ++ T.unpack (toText x)
+  where
+    errs = NE.fromList errs''
+
+-- | The same as @('~~->')@, but expects only one parse error.
+
+infix 2 ~->
+
+(~->)
+  :: Text
+     -- ^ Input for parser
+  -> ParseError Char MMarkErr
+     -- ^ Expected parse error to compare with
+  -> Expectation
+input ~-> err = input ~~-> [err]
+
+-- | Test parser and render by specifying input for parser and expected
+-- output of render.
+
+infix 2 =->
+
+(=->)
+  :: Text              -- ^ Input for MMark parser
+  -> Text              -- ^ Output of render to match against
+  -> Expectation
+input =-> expected =
+  case MMark.parse "" input of
+    Left errs -> expectationFailure $
+      "the parser is expected to succeed, but it failed with:\n" ++
+      MMark.parseErrorsPretty input errs
+    Right factual -> toText factual `shouldBe` expected
+
+-- | Just like @('=->')@, but also appends newline to given input and tries
+-- with that as well.
+
+infix ==->
+
+(==->)
+  :: Text              -- ^ Input for MMark parser
+  -> Text              -- ^ Output of render to match against
+  -> Expectation
+input ==-> expected = do
+  input              =-> expected
+  mappend input "\n" =-> expected
diff --git a/tests/Text/MMarkSpec.hs b/tests/Text/MMarkSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/MMarkSpec.hs
@@ -0,0 +1,1197 @@
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.MMarkSpec (spec) where
+
+import Data.Aeson
+import Data.Char
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Monoid
+import Data.Text (Text)
+import Test.Hspec
+import Test.Hspec.Megaparsec
+import Text.MMark (MMarkErr (..))
+import Text.MMark.Extension (Inline (..))
+import Text.MMark.TestUtils
+import Text.Megaparsec (ErrorFancy (..))
+import qualified Control.Foldl        as L
+import qualified Data.List.NonEmpty   as NE
+import qualified Data.Text            as T
+import qualified Data.Text.IO         as TIO
+import qualified Text.MMark           as MMark
+import qualified Text.MMark.Extension as Ext
+
+spec :: Spec
+spec = parallel $ do
+  describe "parse and render" $ do
+    context "2.2 Tabs" $ do
+      it "CM1" $
+        "\tfoo\tbaz\t\tbim" ==->
+          "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n"
+      it "CM2" $
+        "  \tfoo\tbaz\t\tbim" ==->
+          "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n"
+      it "CM3" $
+        "    a\ta\n    ὐ\ta" ==->
+          "<pre><code>a\ta\nὐ\ta\n</code></pre>\n"
+      xit "CM4" $ -- FIXME pending lists
+        "  - foo\n\n\tbar" ==->
+          "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n"
+      xit "CM5" $ -- FIXME pending lists
+        "- foo\n\n\t\tbar" ==->
+          "<ul>\n<li>\n<p>foo</p>\n<pre><code>  bar\n</code></pre>\n</li>\n</ul>\n"
+      xit "CM6" $ -- FIXME pending blockquotes
+        ">\t\tfoo" ==->
+          "<blockquote>\n<pre><code>  foo\n</code></pre>\n</blockquote>\n"
+      xit "CM7" $ -- FIXME pending lists
+        "-\t\tfoo" ==->
+          "<ul>\n<li>\n<pre><code>  foo\n</code></pre>\n</li>\n</ul>\n"
+      it "CM8" $
+        "    foo\n\tbar" ==->
+          "<pre><code>foo\nbar\n</code></pre>\n"
+      xit "CM9" $ -- FIXME pending lists
+        " - foo\n   - bar\n\t - baz" ==->
+          "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n"
+      it "CM10" $
+        "#\tFoo" ==-> "<h1 id=\"foo\">Foo</h1>\n"
+      it "CM11" $
+        "*\t*\t*\t" ==-> "<hr>\n"
+    context "3.1 Precedence" $
+      xit "CM12" $ -- FIXME pending lists
+        "- `one\n- two`" ==->
+          "<ul>\n<li>`one</li>\n<li>two`</li>\n</ul>\n"
+    context "4.1 Thematic breaks" $ do
+      it "CM13" $
+        "***\n---\n___" ==-> "<hr>\n<hr>\n<hr>\n"
+      it "CM14" $
+        "+++" ==-> "<p>+++</p>\n"
+      it "CM15" $
+        "===" ==-> "<p>===</p>\n"
+      it "CM16" $
+        let s = "--\n**\n__\n"
+        in s ~-> errFancy (posN 4 s) (nonFlanking "*")
+      it "CM17" $
+        " ***\n  ***\n   ***" ==-> "<hr>\n<hr>\n<hr>\n"
+      it "CM18" $
+        "    ***" ==-> "<pre><code>***\n</code></pre>\n"
+      it "CM19" $
+        let s = "Foo\n    ***\n"
+        in s ~-> errFancy (posN 10 s)  (nonFlanking "*")
+      it "CM20" $
+        "_____________________________________" ==->
+          "<hr>\n"
+      it "CM21" $
+        " - - -" ==-> "<hr>\n"
+      it "CM22" $
+        " **  * ** * ** * **" ==-> "<hr>\n"
+      it "CM23" $
+        "-     -      -      -" ==-> "<hr>\n"
+      it "CM24" $
+        "- - - -    " ==-> "<hr>\n"
+      it "CM25" $
+        let s = "_ _ _ _ a\n\na------\n\n---a---\n"
+        in s ~-> errFancy posI (nonFlanking "_")
+      it "CM26" $
+        " *\\-*" ==-> "<p><em>-</em></p>\n"
+      xit "CM27" $ -- FIXME pending lists
+        "- foo\n***\n- bar" ==->
+         "<ul>\n<li>foo</li>\n</ul>\n<hr />\n<ul>\n<li>bar</li>\n</ul>\n"
+      it "CM28" $
+        let s = "Foo\n***\nbar"
+        in s ~-> errFancy (posN 6 s) (nonFlanking "*")
+      xit "CM29" $ -- FIXME pending setext headings
+        "Foo\n---\nbar" ==->
+          "<h2>Foo</h2>\n<p>bar</p>\n"
+      xit "CM30" $ -- FIXME pending lists
+        "* Foo\n* * *\n* Bar" ==->
+          "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n<ul>\n<li>Bar</li>\n</ul>\n"
+      xit "CM31" $ -- FIXME pending lists
+        "- Foo\n- * * *" ==->
+          "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n"
+    context "4.2 ATX headings" $ do
+      it "CM32" $
+        "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo" ==->
+          "<h1 id=\"foo\">foo</h1>\n<h2 id=\"foo\">foo</h2>\n<h3 id=\"foo\">foo</h3>\n<h4 id=\"foo\">foo</h4>\n<h5 id=\"foo\">foo</h5>\n<h6 id=\"foo\">foo</h6>\n"
+      it "CM33" $
+        let s = "####### foo"
+        in s ~-> err (posN 6 s) (utok '#' <> elabel "white space")
+      it "CM34" $
+        let s = "#5 bolt\n\n#hashtag"
+        in s ~~->
+             [ err (posN 1 s)  (utok '5' <> etok '#' <> elabel "white space")
+             , err (posN 10 s) (utok 'h' <> etok '#' <> elabel "white space") ]
+      it "CM35" $
+        "\\## foo" ==-> "<p>## foo</p>\n"
+      it "CM36" $
+        "# foo *bar* \\*baz\\*" ==-> "<h1 id=\"foo-bar-*baz*\">foo <em>bar</em> *baz*</h1>\n"
+      it "CM37" $
+        "#                  foo                     " ==->
+          "<h1 id=\"foo\">foo</h1>\n"
+      it "CM38" $
+        " ### foo\n  ## foo\n   # foo" ==->
+          "<h3 id=\"foo\">foo</h3>\n<h2 id=\"foo\">foo</h2>\n<h1 id=\"foo\">foo</h1>\n"
+      it "CM39" $
+        "    # foo" ==-> "<pre><code># foo\n</code></pre>\n"
+      it "CM40" $
+        "foo\n    # bar" ==-> "<p>foo\n# bar</p>\n"
+      it "CM41" $
+        "## foo ##\n  ###   bar    ###" ==->
+          "<h2 id=\"foo\">foo</h2>\n<h3 id=\"bar\">bar</h3>\n"
+      it "CM42" $
+        "# foo ##################################\n##### foo ##" ==->
+          "<h1 id=\"foo\">foo</h1>\n<h5 id=\"foo\">foo</h5>\n"
+      it "CM43" $
+        "### foo ###     " ==-> "<h3 id=\"foo\">foo</h3>\n"
+      it "CM44" $
+        "### foo ### b" ==-> "<h3 id=\"foo-###-b\">foo ### b</h3>\n"
+      it "CM45" $
+        "# foo#" ==-> "<h1 id=\"foo#\">foo#</h1>\n"
+      it "CM46" $
+        "### foo \\###\n## foo #\\##\n# foo \\#" ==->
+          "<h3 id=\"foo-###\">foo ###</h3>\n<h2 id=\"foo-###\">foo ###</h2>\n<h1 id=\"foo-#\">foo #</h1>\n"
+      it "CM47" $
+        "****\n## foo\n****" ==->
+          "<hr>\n<h2 id=\"foo\">foo</h2>\n<hr>\n"
+      it "CM48" $
+        "Foo bar\n# baz\nBar foo" ==->
+          "<p>Foo bar\n# baz\nBar foo</p>\n"
+      it "CM49" $
+        let s = "## \n#\n### ###"
+        in s ~~->
+             [ err (posN 3 s) (utok '\n' <> elabel "heading character" <> elabel "white space")
+             , err (posN 5 s) (utok '\n' <> etok '#' <> elabel "white space") ]
+    context "4.4 Indented code blocks" $ do
+      it "CM76" $
+        "    a simple\n      indented code block" ==->
+          "<pre><code>a simple\n  indented code block\n</code></pre>\n"
+      xit "CM77" $ -- FIXME pending lists
+        "  - foo\n\n    bar" ==->
+          "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n"
+      xit "CM78" $ -- FIXME pending lists
+        "1.  foo\n\n    - bar" ==->
+           "<ol>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n"
+      it "CM79" $
+        "    <a/>\n    *hi*\n\n    - one" ==->
+          "<pre><code>&lt;a/&gt;\n*hi*\n\n- one\n</code></pre>\n"
+      it "CM80" $
+        "    chunk1\n\n    chunk2\n  \n \n \n    chunk3" ==->
+          "<pre><code>chunk1\n\nchunk2\n\n\n\nchunk3\n</code></pre>\n"
+      it "CM81" $
+        "    chunk1\n      \n      chunk2" ==->
+          "<pre><code>chunk1\n  \n  chunk2\n</code></pre>\n"
+      it "CM82" $
+        "Foo\n    bar\n" ==->
+          "<p>Foo\nbar</p>\n"
+      it "CM83" $
+        "    foo\nbar" ==->
+          "<pre><code>foo\n</code></pre>\n<p>bar</p>\n"
+      xit "CM84" $ -- FIXME pending setext headings
+        "# Heading\n    foo\nHeading\n------\n    foo\n----\n" ==->
+          "<h1>Heading</h1>\n<pre><code>foo\n</code></pre>\n<h2>Heading</h2>\n<pre><code>foo\n</code></pre>\n<hr />\n"
+      it "CM85" $
+        "        foo\n    bar" ==->
+          "<pre><code>    foo\nbar\n</code></pre>\n"
+      it "CM86" $
+        "\n    \n    foo\n    \n" ==->
+          "<pre><code>foo\n</code></pre>\n"
+      it "CM87" $
+        "    foo  " ==->
+          "<pre><code>foo  \n</code></pre>\n"
+    context "4.5 Fenced code blocks" $ do
+      it "CM88" $
+        "```\n<\n >\n```" ==->
+          "<pre><code>&lt;\n &gt;\n</code></pre>\n"
+      it "CM89" $
+        "~~~\n<\n >\n~~~" ==->
+          "<pre><code>&lt;\n &gt;\n</code></pre>\n"
+      it "CM90" $
+        "```\naaa\n~~~\n```" ==->
+          "<pre><code>aaa\n~~~\n</code></pre>\n"
+      it "CM91" $
+        "~~~\naaa\n```\n~~~" ==->
+          "<pre><code>aaa\n```\n</code></pre>\n"
+      it "CM92" $
+        "````\naaa\n```\n``````" ==->
+          "<pre><code>aaa\n```\n</code></pre>\n"
+      it "CM93" $
+        "~~~~\naaa\n~~~\n~~~~" ==->
+          "<pre><code>aaa\n~~~\n</code></pre>\n"
+      it "CM94" $
+        let s = "```"
+        in s ~-> err (posN 3 s)
+           (ueof <> etok '`' <> elabel "info string" <> elabel "newline")
+      it "CM95" $
+        let s = "`````\n\n```\naaa\n"
+        in s ~-> err (posN 15 s)
+           (ueof <> elabel "closing code fence" <> elabel "code block content")
+      xit "CM96" $ -- FIXME pending blockquotes
+        "> ```\n> aaa\n\nbbb" ==->
+          "<blockquote>\n<pre><code>aaa\n</code></pre>\n</blockquote>\n<p>bbb</p>\n"
+      it "CM97" $
+        "```\n\n  \n```" ==->
+          "<pre><code>\n  \n</code></pre>\n"
+      it "CM98" $
+        "```\n```" ==->
+          "<pre><code></code></pre>\n"
+      it "CM99" $
+        " ```\n aaa\naaa\n```" ==->
+          "<pre><code>aaa\naaa\n</code></pre>\n"
+      it "CM100" $
+        "  ```\naaa\n  aaa\naaa\n  ```" ==->
+          "<pre><code>aaa\naaa\naaa\n</code></pre>\n"
+      it "CM101" $
+        "   ```\n   aaa\n    aaa\n  aaa\n   ```" ==->
+          "<pre><code>aaa\n aaa\naaa\n</code></pre>\n"
+      it "CM102" $
+        "    ```\n    aaa\n    ```" ==->
+          "<pre><code>```\naaa\n```\n</code></pre>\n"
+      it "CM103" $
+        "```\naaa\n  ```" ==->
+          "<pre><code>aaa\n</code></pre>\n"
+      it "CM104" $
+        "   ```\naaa\n  ```" ==->
+          "<pre><code>aaa\n</code></pre>\n"
+      it "CM105" $
+        let s  = "```\naaa\n    ```\n"
+        in s ~-> err (posN 16 s)
+           (ueof <> elabel "closing code fence" <> elabel "code block content")
+      it "CM106" $
+        "``` ```\naaa" ==->
+          "<p><code></code>\naaa</p>\n"
+      it "CM107" $
+        let s = "~~~~~~\naaa\n~~~ ~~\n"
+        in s ~-> err (posN 18 s)
+           (ueof <> elabel "closing code fence" <> elabel "code block content")
+      it "CM108" $
+        "foo\n```\nbar\n```\nbaz" ==->
+          "<p>foo\n<code>bar</code>\nbaz</p>\n"
+      xit "CM109" $ -- FIXME pending setext headings
+        "foo\n---\n~~~\nbar\n~~~\n# baz" ==->
+          "<h2>foo</h2>\n<pre><code>bar\n</code></pre>\n<h1>baz</h1>\n"
+      it "CM110" $
+        "```ruby\ndef foo(x)\n  return 3\nend\n```" ==->
+          "<pre><code class=\"language-ruby\">def foo(x)\n  return 3\nend\n</code></pre>\n"
+      it "CM111" $
+        "~~~~    ruby startline=3 $%@#$\ndef foo(x)\n  return 3\nend\n~~~~~~~" ==->
+          "<pre><code class=\"language-ruby\">def foo(x)\n  return 3\nend\n</code></pre>\n"
+      it "CM112" $
+        "````;\n````" ==->
+          "<pre><code class=\"language-;\"></code></pre>\n"
+      it "CM113" $
+        "``` aa ```\nfoo" ==->
+          "<p><code>aa</code>\nfoo</p>\n"
+      it "CM114" $
+        "```\n``` aaa\n```" ==->
+          "<pre><code>``` aaa\n</code></pre>\n"
+    context "4.8 Paragraphs" $ do
+      it "CM180" $
+        "aaa\n\nbbb" ==->
+          "<p>aaa</p>\n<p>bbb</p>\n"
+      it "CM181" $
+        "aaa\nbbb\n\nccc\nddd" ==->
+          "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n"
+      it "CM182" $
+        "aaa\n\n\nbbb" ==->
+          "<p>aaa</p>\n<p>bbb</p>\n"
+      it "CM183" $
+        "  aaa\n bbb" ==->
+          "<p>aaa\nbbb</p>\n"
+      it "CM184" $
+        "aaa\n             bbb\n                                       ccc" ==->
+          "<p>aaa\nbbb\nccc</p>\n"
+      it "CM185" $
+        "   aaa\nbbb" ==-> "<p>aaa\nbbb</p>\n"
+      it "CM186" $
+        "    aaa\nbbb" ==->
+          "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n"
+      it "CM187" $
+        "aaa     \nbbb     " ==->
+          "<p>aaa\nbbb</p>\n"
+    context "4.9 Blank lines" $
+      it "CM188" $
+        "  \n\naaa\n  \n\n# aaa\n\n  " ==->
+          "<p>aaa</p>\n<h1 id=\"aaa\">aaa</h1>\n"
+    context "6 Inlines" $
+      it "CM286" $
+        let s  = "`hi`lo`\n"
+        in s ~-> err (posN 7 s) (ueib <> etok '`' <> elabel "code span content")
+    context "6.1 Blackslash escapes" $ do
+      it "CM287" $
+        "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n"
+          ==-> "<p>!&quot;#$%&amp;&#39;()*+,-./:;&lt;=&gt;?@[\\]^_`{|}~</p>\n"
+      it "CM288" $
+        "\\\t\\A\\a\\ \\3\\φ\\«" ==->
+          "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n"
+      it "CM289" $
+        "\\*not emphasized\\*\n\\<br/> not a tag\n\\[not a link\\](/foo)\n\\`not code\\`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo\\]: /url \"not a reference\"\n" ==->
+        "<p>*not emphasized*\n&lt;br/&gt; not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url &quot;not a reference&quot;</p>\n"
+      it "CM290" $
+        let s = "\\\\*emphasis*"
+        in s ~-> err (posN 2 s) (utok '*' <> eeib <> eric)
+      xit "CM291" $
+        "foo\\\nbar" ==->
+          "<p>foo<br>\nbar</p>\n"
+      it "CM292" $
+        "`` \\[\\` ``" ==->
+          "<p><code>\\[\\`</code></p>\n"
+      it "CM293" $
+        "    \\[\\]" ==->
+          "<pre><code>\\[\\]\n</code></pre>\n"
+      it "CM294" $
+        "~~~\n\\[\\]\n~~~" ==->
+          "<pre><code>\\[\\]\n</code></pre>\n"
+      it "CM295" $
+        "<http://example.com?find=*>" ==->
+          "<p><a href=\"http://example.com/?find=*\">http://example.com/?find=*</a></p>\n"
+      xit "CM296" $ -- FIXME pending HTML inlines
+        "<a href=\"/bar\\/)\">" ==->
+          "<p>&lt;a href=&quot;/bar/)&quot;&gt;</p>\n"
+      it "CM297" $
+        let s = "[foo](/bar\\* \"ti\\*tle\")"
+        in s ~-> err (posN 10 s)
+          (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)
+      xit "CM298" $ -- FIXME pending reference links
+        "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"" ==->
+          "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n"
+      it "CM299" $
+        "``` foo\\+bar\nfoo\n```" ==->
+          "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n"
+    context "6.2 Entity and numeric character references" $
+      xit "CM300" $ -- FIXME pending entity references
+        "&nbsp; &amp; &copy; &AElig; &Dcaron;\n&frac34; &HilbertSpace; &DifferentialD;\n&ClockwiseContourIntegral; &ngE;"
+          ==-> "<p>  &amp; © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n"
+    context "6.3 Code spans" $ do
+      it "CM312" $
+        "`foo`" ==-> "<p><code>foo</code></p>\n"
+      it "CM313" $
+        "`` foo ` bar  ``" ==->
+          "<p><code>foo ` bar</code></p>\n"
+      it "CM314" $
+        "` `` `" ==-> "<p><code>``</code></p>\n"
+      it "CM315" $
+        "``\nfoo\n``" ==-> "<p><code>foo</code></p>\n"
+      it "CM316" $
+        "`foo   bar\n  baz`" ==-> "<p><code>foo bar baz</code></p>\n"
+      it "CM317" $
+        "`a  b`" ==-> "<p><code>a  b</code></p>\n"
+      it "CM318" $
+        "`foo `` bar`" ==-> "<p><code>foo `` bar</code></p>\n"
+      it "CM319" $
+        let s  = "`foo\\`bar`\n"
+        in s ~-> err (posN 10 s) (ueib <> etok '`' <> elabel "code span content")
+      it "CM320" $
+        let s  = "*foo`*`\n"
+        in s ~-> err (posN 7 s) (ueib <> etok '*' <> eic)
+      it "CM321" $
+        let s = "[not a `link](/foo`)\n"
+        in s ~-> err (posN 20 s) (ueib <> etok ']' <> eic <> eric)
+      it "CM322" $
+        let s = "`<a href=\"`\">`\n"
+        in s ~-> err (posN 14 s) (ueib <> etok '`' <> elabel "code span content")
+      xit "CM323" $ -- FIXME pending HTML inlines
+        "<a href=\"`\">`" ==-> "<p><a href=\"`\">`</p>\n"
+      it "CM324" $
+        let s = "`<http://foo.bar.`baz>`\n"
+        in s ~-> err (posN 23 s) (ueib <> etok '`' <> elabel "code span content")
+      it "CM325" $
+        "<http://foo.bar.`baz>`" ==->
+          "<p>&lt;http://foo.bar.<code>baz></code></p>\n"
+      it "CM326" $
+        let s  = "```foo``\n"
+        in s ~-> err (posN 8 s) (ueib <> etok '`' <> elabel "code span content")
+      it "CM327" $
+        let s = "`foo\n"
+        in s ~-> err (posN 4 s) (ueib <> etok '`' <> elabel "code span content")
+      it "CM328" $
+        let s  = "`foo``bar``\n"
+        in s ~-> err (posN 11 s) (ueib <> etok '`' <> elabel "code span content")
+    context "6.4 Emphasis and strong emphasis" $ do
+      it "CM329" $
+        "*foo bar*" ==-> "<p><em>foo bar</em></p>\n"
+      it "CM330" $
+        let s = "a * foo bar*\n"
+        in s ~-> err (posN 2 s) (utok '*' <> eeib <> eric)
+      it "CM331" $
+        let s = "a*\"foo\"*\n"
+        in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)
+      it "CM332" $
+        let s = "* a *\n"
+        in s  ~-> errFancy posI (nonFlanking "*")
+      it "CM333" $
+        let s = "foo*bar*\n"
+        in s ~-> err (posN 3 s) (utok '*' <> eeib <> eric)
+      it "CM334" $
+        let s = "5*6*78\n"
+        in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)
+      it "CM335" $
+        "_foo bar_" ==-> "<p><em>foo bar</em></p>\n"
+      it "CM336" $
+        let s = "_ foo bar_\n"
+        in s ~-> errFancy posI (nonFlanking "_")
+      it "CM337" $
+        let s = "a_\"foo\"_\n"
+        in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)
+      it "CM338" $
+        let s = "foo_bar_\n"
+        in s  ~-> err (posN 3 s) (utok '_' <> eeib <> eric)
+      it "CM339" $
+        let s = "5_6_78\n"
+        in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)
+      it "CM340" $
+        let s = "пристаням_стремятся_\n"
+        in s ~-> err (posN 9 s) (utok '_' <> eeib <> eric)
+      it "CM341" $
+        let s  = "aa_\"bb\"_cc\n"
+        in s ~-> err (posN 2 s) (utok '_' <> eeib <> eric)
+      it "CM342" $
+        let s  = "foo-_(bar)_\n"
+        in s ~-> err (posN 4 s) (utok '_' <> eeib <> eric)
+      it "CM343" $
+        let s = "_foo*\n"
+        in s ~-> err (posN 4 s) (utok '*' <> etok '_' <> eric)
+      it "CM344" $
+        let s = "*foo bar *\n"
+        in s ~-> errFancy (posN 9 s) (nonFlanking "*")
+      it "CM345" $
+        let s = "*foo bar\n*\n"
+        in s ~-> errFancy (posN 9 s) (nonFlanking "*")
+      it "CM346" $
+        let s = "*(*foo)\n"
+        in s ~-> errFancy posI (nonFlanking "*")
+      it "CM347" $
+        let s = "*(*foo*)*\n"
+        in s ~-> errFancy posI (nonFlanking "*")
+      it "CM348" $
+        let s = "*foo*bar\n"
+        in s ~-> errFancy (posN 4 s) (nonFlanking "*")
+      it "CM349" $
+        let s = "_foo bar _\n"
+        in s ~-> errFancy (posN 9 s) (nonFlanking "_")
+      it "CM350" $
+        let s = "_(_foo)\n"
+        in s ~-> errFancy posI (nonFlanking "_")
+      it "CM351" $
+        let s = "_(_foo_)_\n"
+        in s ~-> errFancy posI (nonFlanking "_")
+      it "CM352" $
+        let s = "_foo_bar\n"
+        in s ~-> errFancy (posN 4 s) (nonFlanking "_")
+      it "CM353" $
+        let s = "_пристаням_стремятся\n"
+        in s ~-> errFancy (posN 10 s) (nonFlanking "_")
+      it "CM354" $
+        let s = "_foo_bar_baz_\n"
+        in s ~-> errFancy (posN 4 s) (nonFlanking "_")
+      it "CM355" $
+        "_\\(bar\\)_.\n" ==-> "<p><em>(bar)</em>.</p>\n"
+      it "CM356" $
+        "**foo bar**\n" ==-> "<p><strong>foo bar</strong></p>\n"
+      it "CM357" $
+        let s = "** foo bar**\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "*")
+      it "CM358" $
+        let s = "a**\"foo\"**\n"
+        in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)
+      it "CM359" $
+        let s = "foo**bar**\n"
+        in s ~-> err (posN 3 s) (utok '*' <> eeib <> eric)
+      it "CM360" $
+        "__foo bar__" ==-> "<p><strong>foo bar</strong></p>\n"
+      it "CM361" $
+        let s = "__ foo bar__\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "_")
+      it "CM362" $
+        let s = "__\nfoo bar__\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "_")
+      it "CM363" $
+        let s = "a__\"foo\"__\n"
+        in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)
+      it "CM364" $
+        let s = "foo__bar__\n"
+        in s ~-> err (posN 3 s) (utok '_' <> eeib <> eric)
+      it "CM365" $
+        let s = "5__6__78\n"
+        in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)
+      it "CM366" $
+        let s = "пристаням__стремятся__\n"
+        in s ~-> err (posN 9 s) (utok '_' <> eeib <> eric)
+      it "CM367" $
+        "__foo, __bar__, baz__" ==->
+          "<p><strong>foo, <strong>bar</strong>, baz</strong></p>\n"
+      it "CM368" $
+        "foo-__\\(bar\\)__" ==-> "<p>foo-<strong>(bar)</strong></p>\n"
+      it "CM369" $
+        let s = "**foo bar **\n"
+        in s ~-> errFancy (posN 11 s) (nonFlanking "*")
+      it "CM370" $
+        let s = "**(**foo)\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "*")
+      it "CM371" $
+        let s = "*(**foo**)*\n"
+        in s ~-> errFancy posI (nonFlanking "*")
+      xit "CM372" $ -- FIXME doesn't pass with current approach
+        "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**" ==->
+        "<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\n<em>Asclepias physocarpa</em>)</strong></p>\n"
+      it "CM373" $
+        "**foo \"*bar*\" foo**" ==->
+          "<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>\n"
+      it "CM374" $
+        let s = "**foo**bar\n"
+        in s ~-> errFancy (posN 5 s) (nonFlanking "**")
+      it "CM375" $
+        let s = "__foo bar __\n"
+        in s ~-> errFancy (posN 11 s) (nonFlanking "_")
+      it "CM376" $
+        let s = "__(__foo)\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "_")
+      it "CM377" $
+        let s = "_(__foo__)_\n"
+        in s ~-> errFancy posI (nonFlanking "_")
+      it "CM378" $
+        let s = "__foo__bar\n"
+        in s ~-> errFancy (posN 5 s) (nonFlanking "__")
+      it "CM379" $
+        let s = "__пристаням__стремятся\n"
+        in s ~-> errFancy (posN 11 s) (nonFlanking "__")
+      it "CM380" $
+        "__foo\\_\\_bar\\_\\_baz__" ==->
+          "<p><strong>foo__bar__baz</strong></p>\n"
+      it "CM381" $
+        "__\\(bar\\)__." ==->
+          "<p><strong>(bar)</strong>.</p>\n"
+      it "CM382" $
+        "*foo [bar](/url)*" ==->
+          "<p><em>foo <a href=\"/url\">bar</a></em></p>\n"
+      it "CM383" $
+        "*foo\nbar*" ==->
+          "<p><em>foo\nbar</em></p>\n"
+      it "CM384" $
+        "_foo __bar__ baz_" ==->
+          "<p><em>foo <strong>bar</strong> baz</em></p>\n"
+      it "CM385" $
+        "_foo _bar_ baz_" ==->
+          "<p><em>foo <em>bar</em> baz</em></p>\n"
+      it "CM386" $
+        let s = "__foo_ bar_"
+        in s ~-> err (posN 5 s) (utoks "_ " <> etoks "__" <> eric)
+      it "CM387" $
+        "*foo *bar**" ==->
+          "<p><em>foo <em>bar</em></em></p>\n"
+      it "CM388" $
+        "*foo **bar** baz*" ==->
+          "<p><em>foo <strong>bar</strong> baz</em></p>\n"
+      it "CM389" $
+        let s = "*foo**bar**baz*\n"
+        in s ~-> err (posN 5 s) (utok '*' <> eeib)
+      it "CM390" $
+        "***foo** bar*\n" ==-> "<p><em><strong>foo</strong> bar</em></p>\n"
+      it "CM391" $
+        "*foo **bar***\n" ==-> "<p><em>foo <strong>bar</strong></em></p>\n"
+      it "CM392" $
+        let s = "*foo**bar***\n"
+        in s ~-> err (posN 5 s) (utok '*' <> elabel "end of inline block")
+      it "CM393" $
+        "*foo **bar *baz* bim** bop*\n" ==->
+          "<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\n"
+      it "CM394" $
+        "*foo [*bar*](/url)*\n" ==->
+          "<p><em>foo <a href=\"/url\"><em>bar</em></a></em></p>\n"
+      it "CM395" $
+        let s = "** is not an empty emphasis\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "*")
+      it "CM396" $
+        let s = "**** is not an empty strong emphasis\n"
+        in s ~-> errFancy (posN 3 s) (nonFlanking "*")
+      it "CM397" $
+        "**foo [bar](/url)**" ==->
+          "<p><strong>foo <a href=\"/url\">bar</a></strong></p>\n"
+      it "CM398" $
+        "**foo\nbar**" ==->
+          "<p><strong>foo\nbar</strong></p>\n"
+      it "CM399" $
+        "__foo _bar_ baz__" ==->
+          "<p><strong>foo <em>bar</em> baz</strong></p>\n"
+      it "CM400" $
+        "__foo __bar__ baz__" ==->
+          "<p><strong>foo <strong>bar</strong> baz</strong></p>\n"
+      it "CM401" $
+        "____foo__ bar__" ==->
+          "<p><strong><strong>foo</strong> bar</strong></p>\n"
+      it "CM402" $
+        "**foo **bar****" ==->
+          "<p><strong>foo <strong>bar</strong></strong></p>\n"
+      it "CM403" $
+        "**foo *bar* baz**" ==->
+          "<p><strong>foo <em>bar</em> baz</strong></p>\n"
+      it "CM404" $
+        let s = "**foo*bar*baz**\n"
+        in s ~-> err (posN 5 s) (utoks "*b" <> etoks "**" <> eric)
+      it "CM405" $
+        "***foo* bar**" ==->
+          "<p><strong><em>foo</em> bar</strong></p>\n"
+      it "CM406" $
+        "**foo *bar***" ==->
+          "<p><strong>foo <em>bar</em></strong></p>\n"
+      it "CM407" $
+        "**foo *bar **baz**\nbim* bop**" ==->
+          "<p><strong>foo <em>bar <strong>baz</strong>\nbim</em> bop</strong></p>\n"
+      it "CM408" $
+        "**foo [*bar*](/url)**" ==->
+          "<p><strong>foo <a href=\"/url\"><em>bar</em></a></strong></p>\n"
+      it "CM409" $
+        let s = "__ is not an empty emphasis\n"
+        in s ~-> errFancy (posN 1 s) (nonFlanking "_")
+      it "CM410" $
+        let s = "____ is not an empty strong emphasis\n"
+        in s ~-> errFancy (posN 3 s) (nonFlanking "_")
+      it "CM411" $
+        let s = "foo ***\n"
+        in s ~-> errFancy (posN 6 s) (nonFlanking "*")
+      it "CM412" $
+        "foo *\\**" ==-> "<p>foo <em>*</em></p>\n"
+      it "CM413" $
+        "foo *\\_*\n" ==-> "<p>foo <em>_</em></p>\n"
+      it "CM414" $
+        let s = "foo *****\n"
+        in s ~-> errFancy (posN 8 s) (nonFlanking "*")
+      it "CM415" $
+        "foo **\\***" ==-> "<p>foo <strong>*</strong></p>\n"
+      it "CM416" $
+        "foo **\\_**\n" ==-> "<p>foo <strong>_</strong></p>\n"
+      it "CM417" $
+        let s = "**foo*\n"
+        in s ~-> err (posN 5 s) (utok '*' <> etoks "**" <> eric)
+      it "CM418" $
+        let s = "*foo**\n"
+        in s ~-> err (posN 5 s) (utok '*' <> eeib)
+      it "CM419" $
+        let s = "***foo**\n"
+        in s ~-> err (posN 8 s) (ueib <> etok '*' <> eic)
+      it "CM420" $
+        let s = "****foo*\n"
+        in s ~-> err (posN 7 s) (utok '*' <> etoks "**" <> eric)
+      it "CM421" $
+        let s = "**foo***\n"
+        in s ~-> err (posN 7 s) (utok '*' <> eeib)
+      it "CM422" $
+        let s = "*foo****\n"
+        in s ~-> err (posN 5 s) (utok '*' <> eeib)
+      it "CM423" $
+        let s = "foo ___\n"
+        in s ~-> errFancy (posN 6 s) (nonFlanking "_")
+      it "CM424" $
+        "foo _\\__" ==-> "<p>foo <em>_</em></p>\n"
+      it "CM425" $
+        "foo _\\*_" ==-> "<p>foo <em>*</em></p>\n"
+      it "CM426" $
+        let s = "foo _____\n"
+        in s ~-> errFancy (posN 8 s) (nonFlanking "_")
+      it "CM427" $
+        "foo __\\___" ==-> "<p>foo <strong>_</strong></p>\n"
+      it "CM428" $
+        "foo __\\*__" ==-> "<p>foo <strong>*</strong></p>\n"
+      it "CM429" $
+        let s = "__foo_\n"
+        in s ~-> err (posN 5 s) (utok '_' <> etoks "__" <> eric)
+      it "CM430" $
+        let s = "_foo__\n"
+        in s ~-> err (posN 5 s) (utok '_' <> eeib)
+      it "CM431" $
+        let s = "___foo__\n"
+        in s ~-> err (posN 8 s) (ueib <> etok '_' <> eic)
+      it "CM432" $
+        let s = "____foo_\n"
+        in s ~-> err (posN 7 s) (utok '_' <> etoks "__" <> eric)
+      it "CM433" $
+        let s = "__foo___\n"
+        in s ~-> err (posN 7 s) (utok '_' <> eeib)
+      it "CM434" $
+        let s = "_foo____\n"
+        in s ~-> err (posN 5 s) (utok '_' <> eeib)
+      it "CM435" $
+        "**foo**" ==-> "<p><strong>foo</strong></p>\n"
+      it "CM436" $
+        "*_foo_*" ==-> "<p><em><em>foo</em></em></p>\n"
+      it "CM437" $
+        "__foo__" ==-> "<p><strong>foo</strong></p>\n"
+      it "CM438" $
+        "_*foo*_" ==-> "<p><em><em>foo</em></em></p>\n"
+      it "CM439" $
+        "****foo****" ==-> "<p><strong><strong>foo</strong></strong></p>\n"
+      it "CM440" $
+        "____foo____" ==-> "<p><strong><strong>foo</strong></strong></p>\n"
+      it "CM441" $
+        "******foo******" ==->
+          "<p><strong><strong><strong>foo</strong></strong></strong></p>\n"
+      it "CM442" $
+        "***foo***" ==-> "<p><em><strong>foo</strong></em></p>\n"
+      it "CM443" $
+        "_____foo_____" ==->
+          "<p><strong><strong><em>foo</em></strong></strong></p>\n"
+      it "CM444" $
+        let s = "*foo _bar* baz_\n"
+        in s ~-> err (posN 9 s) (utok '*' <> etok '_' <> eric)
+      it "CM445" $
+        let s = "*foo __bar *baz bim__ bam*\n"
+        in s ~-> err (posN 19 s) (utok '_' <> etok '*' <> eric)
+      it "CM446" $
+        let s = "**foo **bar baz**\n"
+        in s ~-> err (posN 17 s) (ueib <> etoks "**" <> eic)
+      it "CM447" $
+        let s = "*foo *bar baz*\n"
+        in s ~-> err (posN 14 s) (ueib <> etok '*' <> eic)
+      it "CM448" $
+        let s = "*[bar*](/url)\n"
+        in s ~-> err (posN 5 s) (utok '*' <> etok ']' <> eric)
+      it "CM449" $
+        let s = "_foo [bar_](/url)\n"
+        in s ~-> err (posN 9 s) (utok '_' <> etok ']' <> eric)
+      xit "CM450" $ -- FIXME pending HTML inlines
+        "*<img src=\"foo\" title=\"*\"/>" ==->
+          "<p>*<img src=\"foo\" title=\"*\"/></p>\n"
+      xit "CM451" $ -- FIXME pending HTML inlines
+        "**<a href=\"**\">" ==-> "<p>**<a href=\"**\"></p>\n"
+      xit "CM452" $
+        "__<a href=\"__\">\n" ==-> "<p>__<a href=\"__\"></p>\n"
+      it "CM453" $
+        "*a `*`*" ==-> "<p><em>a <code>*</code></em></p>\n"
+      it "CM454" $
+        "_a `_`_" ==-> "<p><em>a <code>_</code></em></p>\n"
+      it "CM455" $
+        let s = "**a<http://foo.bar/?q=**>"
+        in s ~-> err (posN 25 s) (ueib <> etoks "**" <> eic)
+      it "CM456" $
+        let s = "__a<http://foo.bar/?q=__>"
+        in s ~-> err (posN 26 s) (ueib <> etoks "__" <> eic)
+    context "6.5 Links" $ do
+      it "CM457" $
+        "[link](/uri \"title\")" ==->
+          "<p><a href=\"/uri\" title=\"title\">link</a></p>\n"
+      it "CM458" $
+        "[link](/uri)" ==->
+          "<p><a href=\"/uri\">link</a></p>\n"
+      it "CM459" $
+        "[link]()" ==->
+          "<p><a href>link</a></p>\n"
+      it "CM460" $
+        "[link](<>)" ==->
+          "<p><a href>link</a></p>\n"
+      it "CM461" $
+        let s = "[link](/my uri)\n"
+        in s ~-> err (posN 11 s)
+           (utok 'u' <> etok '"' <> etok '\'' <> etok '(' <> elabel "white space")
+      it "CM462" $
+        let s = "[link](</my uri>)\n"
+        in s ~-> err (posN 11 s)
+           (utok ' ' <> etok '#' <> etok '/' <> etok '>' <> etok '?' <> eppi)
+      it "CM463" $
+        let s = "[link](foo\nbar)\n"
+        in s ~-> err (posN 11 s)
+           (utok 'b' <> etok '"' <> etok '\'' <> etok '(' <> elabel "white space")
+      it "CM464" $
+        let s = "[link](<foo\nbar>)\n"
+        in s ~-> err (posN 11 s)
+           (utok '\n' <> etok '#' <> etok '/' <> etok '>' <> etok '?' <> eppi)
+      it "CM465" $
+        let s = "[link](\\(foo\\))"
+        in s ~-> err (posN 7 s) (utok '\\' <> etoks "//" <> etok '#' <>
+             etok '/' <> etok '<' <> etok '?' <> elabel "ASCII alpha character" <>
+             euri <> elabel "path piece")
+      it "CM466" $
+        "[link](foo(and(bar)))\n" ==->
+          "<p><a href=\"foo(and(bar\">link</a>))</p>\n"
+      it "CM467" $
+        let s = "[link](foo\\(and\\(bar\\))"
+        in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)
+      it "CM468" $
+        "[link](<foo(and(bar)>)" ==->
+          "<p><a href=\"foo(and(bar)\">link</a></p>\n"
+      it "CM469" $
+        let s = "[link](foo\\)\\:)"
+        in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)
+      it "CM470" $
+        "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n"
+          ==-> "<p><a href=\"#fragment\">link</a></p>\n<p><a href=\"http://example.com/#fragment\">link</a></p>\n<p><a href=\"http://example.com/?foo=3#frag\">link</a></p>\n"
+      it "CM471" $
+        let s = "[link](foo\\bar)"
+        in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)
+      xit "CM472" $ -- FIXME pending entity references
+        "[link](foo%20b&auml;)"
+          ==-> "<p><a href=\"foo%20b&amp;auml;\">link</a></p>\n"
+      it "CM473" $
+        let s = "[link](\"title\")"
+        in s ~-> err (posN 7 s)
+             (utok '"' <> etoks "//" <> etok '#' <> etok '/' <> etok '<' <>
+              etok '?' <> elabel "ASCII alpha character" <> euri <> elabel "path piece")
+      it "CM474" $
+        "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))" ==->
+          "<p><a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a></p>\n"
+      xit "CM475" $ -- FIXME pending entity references
+        "[link](/url \"title \\\"&quot;\")\n" ==->
+          "<p><a href=\"/url\" title=\"title &quot;&quot;\">link</a></p>\n"
+      it "CM476" $
+        let s = "[link](/url \"title\")"
+        in s ~-> err (posN 11 s)
+             (utok ' ' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)
+      it "CM477" $
+        let s = "[link](/url \"title \"and\" title\")\n"
+        in s ~-> err (posN 20 s) (utok 'a' <> etok ')' <> elabel "white space")
+      it "CM478" $
+        "[link](/url 'title \"and\" title')" ==->
+          "<p><a href=\"/url\" title=\"title &quot;and&quot; title\">link</a></p>\n"
+      it "CM479" $
+        "[link](   /uri\n  \"title\"  )" ==->
+          "<p><a href=\"/uri\" title=\"title\">link</a></p>\n"
+      it "CM480" $
+        let s = "[link] (/uri)\n"
+        in s ~-> err (posN 6 s) (utok ' ' <> etok '(')
+      it "CM481" $
+        let s = "[link [foo [bar]]](/uri)\n"
+        in s ~-> err (posN 6 s) (utok '[' <> etok ']' <> eic <> eric)
+      it "CM482" $
+        let s = "[link] bar](/uri)\n"
+        in s ~-> err (posN 6 s) (utok ' ' <> etok '(')
+      it "CM483" $
+        let s = "[link [bar](/uri)\n"
+        in s ~-> err (posN 6 s) (utok '[' <> etok ']' <> eic <> eric)
+      it "CM484" $
+        "[link \\[bar](/uri)\n" ==->
+          "<p><a href=\"/uri\">link [bar</a></p>\n"
+      it "CM485" $
+        "[link *foo **bar** `#`*](/uri)" ==->
+          "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n"
+      it "CM486" $
+        "[![moon](moon.jpg)](/uri)" ==->
+          "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\"></a></p>\n"
+      it "CM487" $
+        let s = "[foo [bar](/uri)](/uri)\n"
+        in s ~-> err (posN 5 s) (utok '[' <> etok ']' <> eic <> eric)
+      it "CM488" $
+        let s = "[foo *[bar [baz](/uri)](/uri)*](/uri)\n"
+        in s ~-> err (posN 11 s) (utok '[' <> etok ']' <> eic <> eric)
+      it "CM489" $
+        let s = "![[[foo](uri1)](uri2)](uri3)"
+        in s ~-> err (posN 3 s) (utoks "[foo" <> eeib <> eic)
+      it "CM490" $
+        let s = "*[foo*](/uri)\n"
+        in s ~-> err (posN 5 s) (utok '*' <> etok ']' <> eric)
+      it "CM491" $
+        let s = "[foo *bar](baz*)\n"
+        in s ~-> err (posN 9 s) (utok ']' <> etok '*' <> eic <> eric)
+      it "CM492" $
+        let s = "*foo [bar* baz]\n"
+        in s ~-> err (posN 9 s) (utok '*' <> etok ']' <> eric)
+      xit "CM493" $ -- FIXME pending inline HTML
+        let s = "[foo <bar attr=\"](baz)\">"
+        in s ~-> err (posN 5 s) (utok '<' <> etok ']')
+      it "CM494" $
+        let s = "[foo`](/uri)`\n"
+        in s ~-> err (posN 13 s) (ueib <> etok ']' <> eic)
+      it "CM495" $
+        "[foo<http://example.com/?search=](uri)>" ==->
+          "<p><a href=\"uri\">foo&lt;http://example.com/?search=</a>&gt;</p>\n"
+    context "6.6 Images" $ do
+      it "CM541" $
+        "![foo](/url \"title\")" ==->
+          "<p><img src=\"/url\" title=\"title\" alt=\"foo\"></p>\n"
+      it "CM542" $
+        "![foo *bar*](train.jpg \"train & tracks\")" ==->
+          "<p><img src=\"train.jpg\" title=\"train &amp; tracks\" alt=\"foo bar\"></p>\n"
+      it "CM543" $
+        let s = "![foo ![bar](/url)](/url2)\n"
+        in s ~-> err (posN 6 s) (utok '!' <> etok ']')
+      it "CM544" $
+        "![foo [bar](/url)](/url2)" ==->
+          "<p><img src=\"/url2\" alt=\"foo bar\"></p>\n"
+      it "CM545" pending
+      it "CM546" pending
+      it "CM547" $
+        "![foo](train.jpg)" ==->
+          "<p><img src=\"train.jpg\" alt=\"foo\"></p>\n"
+      it "CM548" $
+        "My ![foo bar](/path/to/train.jpg  \"title\"   )" ==->
+          "<p>My <img src=\"/path/to/train.jpg\" title=\"title\" alt=\"foo bar\"></p>\n"
+      it "CM549" $
+        "![foo](<url>)" ==->
+          "<p><img src=\"url\" alt=\"foo\"></p>\n"
+      it "CM550" $
+        "![](/url)" ==-> "<p><img src=\"/url\" alt></p>\n"
+      it "CM551-CM562" pending -- pending reference-style stuff
+    context "6.7 Autolinks" $ do
+      it "CM563" $
+        "<http://foo.bar.baz>" ==->
+          "<p><a href=\"http://foo.bar.baz/\">http://foo.bar.baz/</a></p>\n"
+      it "CM564" $
+        "<http://foo.bar.baz/test?q=hello&id=22&boolean>" ==->
+          "<p><a href=\"http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean\">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>\n"
+      it "CM565" $
+        "<irc://foo.bar:2233/baz>" ==->
+          "<p><a href=\"irc://foo.bar:2233/baz\">irc://foo.bar:2233/baz</a></p>\n"
+      it "CM566" $
+        "<MAILTO:FOO@BAR.BAZ>" ==->
+          "<p><a href=\"mailto:FOO@BAR.BAZ\">FOO@BAR.BAZ</a></p>\n"
+      it "CM567" $
+        "<a+b+c:d>" ==->
+          "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n"
+      it "CM568" $
+        "<made-up-scheme://foo,bar>" ==->
+          "<p><a href=\"made-up-scheme://foo/,bar\">made-up-scheme://foo/,bar</a></p>\n"
+      it "CM569" $
+        "<http://../>" ==->
+          "<p>&lt;http://../&gt;</p>\n"
+      it "CM570" $
+        "<localhost:5001/foo>" ==->
+          "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n"
+      it "CM571" $
+        "<http://foo.bar/baz bim>\n" ==->
+          "<p>&lt;http://foo.bar/baz bim&gt;</p>\n"
+      it "CM572" $
+        "<http://example.com/\\[\\>" ==->
+          "<p>&lt;http://example.com/[&gt;</p>\n"
+      it "CM573" $
+        "<foo@bar.example.com>" ==->
+          "<p><a href=\"mailto:foo@bar.example.com\">foo@bar.example.com</a></p>\n"
+      it "CM574" $
+        "<foo+special@Bar.baz-bar0.com>" ==->
+          "<p><a href=\"mailto:foo+special@Bar.baz-bar0.com\">foo+special@Bar.baz-bar0.com</a></p>\n"
+      it "CM575" $
+        "<foo\\+@bar.example.com>" ==->
+          "<p>&lt;foo+@bar.example.com&gt;</p>\n"
+      it "CM576" $
+        "<>" ==->
+          "<p>&lt;&gt;</p>\n"
+      it "CM577" $
+        "< http://foo.bar >" ==->
+          "<p>&lt; http://foo.bar &gt;</p>\n"
+      it "CM578" $
+        "<m:abc>" ==->
+          "<p><a href=\"m:abc\">m:abc</a></p>\n"
+      it "CM579" $
+        "<foo.bar.baz>" ==->
+          "<p><a href=\"foo.bar.baz\">foo.bar.baz</a></p>\n"
+      it "CM580" $
+        "http://example.com" ==->
+          "<p>http://example.com</p>\n"
+      it "CM581" $
+        "foo@bar.example.com" ==->
+          "<p>foo@bar.example.com</p>\n"
+    context "6.9 Hard line breaks" $ do
+      -- NOTE We currently do not support hard line breaks represented in
+      -- markup as space before newline.
+      xit "CM603" $
+        "foo  \nbaz" ==-> "<p>foo<br>\nbaz</p>\n"
+      it "CM604" $
+        "foo\\\nbaz\n" ==-> "<p>foo<br>\nbaz</p>\n"
+      xit "CM605" $
+         "foo       \nbaz" ==-> "<p>foo<br>\nbaz</p>\n"
+      xit "CM606" $
+        "foo  \n     bar" ==-> "<p>foo<br>\nbar</p>\n"
+      it "CM607" $
+        "foo\\\n     bar" ==-> "<p>foo<br>\nbar</p>\n"
+      xit "CM608" $
+        "*foo  \nbar*" ==-> "<p><em>foo<br>\nbar</em></p>\n"
+      it "CM609" $
+        "*foo\\\nbar*" ==-> "<p><em>foo<br>\nbar</em></p>\n"
+      it "CM610" $
+        "`code  \nspan`" ==-> "<p><code>code span</code></p>\n"
+      it "CM611" $
+        "`code\\\nspan`" ==-> "<p><code>code\\ span</code></p>\n"
+      xit "CM612" $
+        "<a href=\"foo  \nbar\">" ==-> "<p><a href=\"foo  \nbar\"></p>\n"
+      xit "CM613" $ -- FIXME pending HTML inlines
+        "<a href=\"foo\\\nbar\">" ==-> "<p><a href=\"foo\\\nbar\"></p>\n"
+      it "CM614" $
+        "foo\\" ==-> "<p>foo\\</p>\n"
+      xit "CM615" $
+        "foo  " ==-> "<p>foo</p>\n"
+      it "CM616" $
+        "### foo\\" ==-> "<h3 id=\"foo\\\">foo\\</h3>\n"
+      it "CM617" $
+        "### foo  " ==-> "<h3 id=\"foo\">foo</h3>\n"
+    context "6.10 Soft line breaks" $ do
+      it "CM618" $
+        "foo\nbaz" ==-> "<p>foo\nbaz</p>\n"
+      it "CM619" $
+        "foo \n baz" ==-> "<p>foo\nbaz</p>\n"
+    context "6.11 Textual content" $ do
+      it "CM620" $
+        "hello $.;'there" ==-> "<p>hello $.;&#39;there</p>\n"
+      it "CM621" $
+        "Foo χρῆν" ==-> "<p>Foo χρῆν</p>\n"
+      it "CM622" $
+        "Multiple     spaces" ==-> "<p>Multiple     spaces</p>\n"
+    -- NOTE I don't test these so extensively because they share
+    -- implementation with emphasis and strong emphasis which are thoroughly
+    -- tested already.
+    context "strikeout" $ do
+      it "works in simplest form" $
+        "It's ~~bad~~ news." ==->
+          "<p>It&#39;s <del>bad</del> news.</p>\n"
+      it "combines with emphasis" $
+        "**It's ~~bad~~** news." ==->
+          "<p><strong>It&#39;s <del>bad</del></strong> news.</p>\n"
+      it "interacts with subscript reasonably (1)" $
+        "It's ~~~bad~~ news~." ==->
+          "<p>It&#39;s <sub><del>bad</del> news</sub>.</p>\n"
+      it "interacts with subscript reasonably (2)" $
+        "It's ~~~bad~ news~~." ==->
+          "<p>It&#39;s <del><sub>bad</sub> news</del>.</p>\n"
+    context "subscript" $ do
+      it "works in simplest form" $
+        "It's ~bad~ news." ==->
+          "<p>It&#39;s <sub>bad</sub> news.</p>\n"
+      it "combines with emphasis" $
+        "**It's ~bad~** news." ==->
+          "<p><strong>It&#39;s <sub>bad</sub></strong> news.</p>\n"
+    context "superscript" $ do
+      it "works in simplest form" $
+        "It's ^bad^ news." ==->
+          "<p>It&#39;s <sup>bad</sup> news.</p>\n"
+      it "combines with emphasis" $
+        "**It's ^bad^** news." ==->
+          "<p><strong>It&#39;s <sup>bad</sup></strong> news.</p>\n"
+      it "a composite, complex example" $
+        "***Something ~~~is not~~ going~ ^so well^** today*." ==->
+          "<p><em><strong>Something <sub><del>is not</del> going</sub> <sup>so well</sup></strong> today</em>.</p>\n"
+    context "multiple parse errors" $
+      it "they are reported in correct order" $ do
+        let s = "Foo `\n\nBar `.\n"
+            pe = ueib <> etok '`' <> elabel "code span content"
+        s ~~->
+          [ err (posN 5  s) pe
+          , err (posN 13 s) pe ]
+    context "given a complete, comprehensive document" $
+      it "outputs expected the HTML fragment" $
+        withFiles "data/comprehensive.md" "data/comprehensive.html"
+  describe "parseErrorsPretty" $
+    it "renders parse errors correctly" $ do
+      let s = "Foo\nBar\nBaz\n"
+          e0 = err posI       (utok 'F' <> etok 'Z')
+          e1 = err (posN 4 s) (utok 'B' <> etok 'Z')
+          e2 = err (posN 8 s) (utok 'B' <> etok 'Z')
+      MMark.parseErrorsPretty s (e0:|[e1,e2]) `shouldBe`
+        "1:1:\n  |\n1 | Foo\n  | ^\nunexpected 'F'\nexpecting 'Z'\n2:1:\n  |\n2 | Bar\n  | ^\nunexpected 'B'\nexpecting 'Z'\n3:1:\n  |\n3 | Baz\n  | ^\nunexpected 'B'\nexpecting 'Z'\n"
+  describe "useExtension" $
+    it "applies given extension" $ do
+      doc <- mkDoc "Here we go."
+      toText (MMark.useExtension (append_ext "..") doc) `shouldBe`
+        "<p>Here we go...</p>\n"
+  describe "useExtensions" $
+    it "applies extensions in the right order" $ do
+      doc <- mkDoc "Here we go."
+      let exts =
+            [ append_ext "3"
+            , append_ext "2"
+            , append_ext "1" ]
+      toText (MMark.useExtensions exts doc) `shouldBe`
+        "<p>Here we go.123</p>\n"
+  describe "runScanner and scanner" $
+    it "extracts information from markdown document" $ do
+      doc <- mkDoc "Here we go, pals."
+      let n = MMark.runScanner doc (length_scan (const True))
+      n `shouldBe` 17
+  describe "combining of scanners" $
+    it "combines scanners" $ do
+      doc <- mkDoc "Here we go, pals."
+      let scan = (,,)
+            <$> length_scan (const True)
+            <*> length_scan isSpace
+            <*> length_scan isPunctuation
+          r = MMark.runScanner doc scan
+      r `shouldBe` (17, 3, 2)
+  describe "projectYaml" $ do
+    context "when document does not contain a YAML section" $
+      it "returns Nothing" $ do
+        doc <- mkDoc "Here we go."
+        MMark.projectYaml doc `shouldBe` Nothing
+    context "when document contains a YAML section" $ do
+      context "when it is valid" $
+        it "returns the YAML section" $ do
+          doc <- mkDoc "---\nx: 100\ny: 200\n---Here we go."
+          let r = object
+                [ "x" .= Number 100
+                , "y" .= Number 200 ]
+          MMark.projectYaml doc `shouldBe` Just r
+      context "when it is invalid" $
+        it "signal correct parse error" $
+          let s = "---\nx: 100\ny: x:\n---Here we go."
+          in s ~-> errFancy (posN 15 s)
+             (fancy . ErrorCustom . YamlParseError $
+              "mapping values are not allowed in this context")
+
+----------------------------------------------------------------------------
+-- Testing extensions
+
+-- | Append given text to all 'Plain' blocks.
+
+append_ext :: Text -> MMark.Extension
+append_ext y = Ext.inlineTrans $ \case
+  Plain x -> Plain (x <> y)
+  other   -> other
+
+----------------------------------------------------------------------------
+-- Testing scanners
+
+-- | Scan total number of characters satisfying a predicate in all 'Plain'
+-- inlines.
+
+length_scan :: (Char -> Bool) -> L.Fold (Ext.Block (NonEmpty Inline)) Int
+length_scan p = Ext.scanner 0 $ \n block ->
+  getSum $ Sum n <> foldMap (foldMap f) block
+  where
+    f (Plain txt) = (Sum . T.length) (T.filter p txt)
+    f _           = mempty
+
+----------------------------------------------------------------------------
+-- For testing with documents loaded externally
+
+-- | Load a complete markdown document from an external file and compare the
+-- final HTML rendering with contents of another file.
+
+withFiles
+  :: FilePath          -- ^ Markdown document
+  -> FilePath          -- ^ HTML document containing the correct result
+  -> Expectation
+withFiles input output = do
+  i <- TIO.readFile input
+  o <- TIO.readFile output
+  i ==-> o
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Unexpected end of inline block.
+
+ueib :: Ord t => ET t
+ueib = ulabel "end of inline block"
+
+-- | Expecting end of inline block.
+
+eeib :: Ord t => ET t
+eeib = elabel "end of inline block"
+
+-- | Expecting end of URI literal.
+
+euri :: Ord t => ET t
+euri = elabel "end of URI literal"
+
+-- | Expecting the rest of path piece.
+
+eppi :: Ord t => ET t
+eppi = elabel "the rest of path piece"
+
+-- | Expecting inline content.
+
+eic :: Ord t => ET t
+eic = elabel "inline content"
+
+-- | Expecting rest of inline content. Eric!
+
+eric :: Ord t => ET t
+eric = elabel "the rest of inline content"
+
+-- | Create a error component complaining that the given 'Text' is not in
+-- left- or right- flanking position.
+
+nonFlanking :: Text -> EF MMarkErr
+nonFlanking = fancy . ErrorCustom . NonFlankingDelimiterRun . NE.fromList . T.unpack
