diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## MMark Ext 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,24 @@
+# MMark Extensions
+
+[![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-ext.svg?style=flat)](https://hackage.haskell.org/package/mmark-ext)
+[![Stackage Nightly](http://stackage.org/package/mmark-ext/badge/nightly)](http://stackage.org/nightly/package/mmark-ext)
+[![Stackage LTS](http://stackage.org/package/mmark-ext/badge/lts)](http://stackage.org/lts/package/mmark-ext)
+[![Build Status](https://travis-ci.org/mrkkrp/mmark-ext.svg?branch=master)](https://travis-ci.org/mrkkrp/mmark-ext)
+[![Coverage Status](https://coveralls.io/repos/mrkkrp/mmark-ext/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/mmark-ext?branch=master)
+
+Commonly useful extensions for
+[MMark](https://hackage.haskell.org/package/mmark) markdown processor.
+
+## Contribution
+
+Issues, bugs, and questions may be reported in [the GitHub issue tracker for
+this project](https://github.com/mrkkrp/mmark-ext/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/Extension/Common.hs b/Text/MMark/Extension/Common.hs
new file mode 100644
--- /dev/null
+++ b/Text/MMark/Extension/Common.hs
@@ -0,0 +1,268 @@
+-- |
+-- Module      :  Text.MMark.Extension.Common
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Commonly useful extensions for MMark markdown processor.
+--
+-- We suggest using a qualified import, like this:
+--
+-- > import qualified Text.MMark.Extension.Common as Ext
+--
+-- Here is an example that uses several extensions from this module at the
+-- same time, it should give you an idea where to start:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- >
+-- > module Main (main) where
+-- >
+-- > import Data.Default.Class
+-- > import qualified Data.Text.IO                as T
+-- > import qualified Data.Text.Lazy.IO           as TL
+-- > import qualified Lucid                       as L
+-- > import qualified Text.MMark                  as MMark
+-- > import qualified Text.MMark.Extension.Common as Ext
+-- >
+-- > main :: IO ()
+-- > main = do
+-- >   let input = "input.md"
+-- >   txt <- T.readFile input
+-- >   case MMark.parse input txt of
+-- >     Left errs -> putStrLn (MMark.parseErrorsPretty txt errs)
+-- >     Right r ->
+-- >       let toc = MMark.runScanner r (Ext.tocScanner 4)
+-- >       in TL.writeFile "output.html"
+-- >           . L.renderText
+-- >           . MMark.render
+-- >           . MMark.useExtensions
+-- >               [ Ext.toc "toc" toc
+-- >               , Ext.punctuationPrettifier def
+-- >               , Ext.obfuscateEmail "protected-email"
+-- >               , Ext.fontAwesome ]
+-- >           $ r
+
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE LambdaCase         #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RecordWildCards    #-}
+
+module Text.MMark.Extension.Common
+  ( -- * Table of contents
+    -- $table-of-contents
+    Toc
+  , tocScanner
+  , toc
+    -- * Punctuation prettifier
+  , Punctuation (..)
+  , punctuationPrettifier
+    -- * Email address obfuscation
+  , obfuscateEmail
+    -- * Font Awesome icons
+  , fontAwesome )
+where
+
+import Data.Data (Data)
+import Data.Default.Class
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Maybe (maybeToList, fromJust)
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import GHC.Generics
+import Lucid
+import Text.MMark
+import Text.MMark.Extension (Bni, Block (..), Inline (..))
+import qualified Control.Foldl        as L
+import qualified Data.List.NonEmpty   as NE
+import qualified Data.Text            as T
+import qualified Text.MMark.Extension as Ext
+import qualified Text.URI             as URI
+
+----------------------------------------------------------------------------
+-- Table of contents
+
+-- $table-of-contents
+--
+-- Place this markup in markdown document where you want table of contents
+-- to be inserted:
+--
+-- > ```toc
+-- > ```
+--
+-- You may use something different than @\"toc\"@ as the info string of the
+-- code block.
+
+-- | An opaque type representing table of contents produced by the
+-- 'tocScanner' scanner.
+
+newtype Toc = Toc [(Int, NonEmpty Inline)]
+
+-- | The scanner builds table of contents 'Toc' that can then be passed to
+-- 'toc' to obtain an extension that renders the table of contents in HTML.
+--
+-- __Note__: Top level header (level 1) is never added to the table of
+-- contents. Open an issue if you think it's not a good behavior.
+
+tocScanner
+  :: Int -- ^ Up to which level (inclusive) to collect headers? Values from
+         -- 2 to 6 make sense here.
+  -> L.Fold Bni Toc
+tocScanner cutoff = fmap (Toc . reverse) . Ext.scanner [] $ \xs block ->
+  case block of
+    Heading2 x -> f 2 x xs
+    Heading3 x -> f 3 x xs
+    Heading4 x -> f 4 x xs
+    Heading5 x -> f 5 x xs
+    Heading6 x -> f 6 x xs
+    _          -> xs
+  where
+    f n a as =
+      if n > cutoff
+        then as
+        else (n, a) : as
+
+-- | Create an extension that replaces a certain code block with previously
+-- constructed table of contents.
+
+toc
+  :: Text -- ^ Label of the code block to replace by the table of contents
+  -> Toc  -- ^ Previously generated by 'tocScanner'
+  -> Extension
+toc label (Toc xs) = Ext.blockTrans $ \case
+  old@(CodeBlock mlabel _) ->
+    case NE.nonEmpty xs of
+      Nothing -> old
+      Just ns ->
+        if mlabel == pure label
+          then renderToc ns
+          else old
+  other -> other
+
+-- | Construct 'Bni' for a table of contents from given collection of
+-- headers. This is a non-public helper.
+
+renderToc :: NonEmpty (Int, NonEmpty Inline) -> Bni
+renderToc = UnorderedList . NE.unfoldr f
+  where
+    f ((n,x) :| xs) =
+      let (sitems, fitems) = span ((> n) . fst) xs
+          url = Ext.headerFragment (Ext.headerId x)
+      in ( Naked (Link x url Nothing :| [])
+           : maybeToList (renderToc <$> NE.nonEmpty sitems)
+         , NE.nonEmpty fitems )
+
+----------------------------------------------------------------------------
+-- Punctuation prettifier
+
+-- | Settings for the punctuation-prettifying extension.
+
+data Punctuation = Punctuation
+  { punctEnDash :: !Bool
+    -- ^ Whether to replace double hyphen @--@ by an en dash @–@ (default:
+    -- 'True')
+  , punctEmDash :: !Bool
+    -- ^ Whether to replace triple hyphen @---@ by an em dash @—@ (default:
+    -- 'True')
+  } deriving (Eq, Ord, Show, Read, Data, Typeable, Generic)
+
+instance Default Punctuation where
+  def = Punctuation
+    { punctEnDash = True
+    , punctEmDash = True }
+
+-- | Prettify punctuation according to the settings in 'Punctuation'.
+
+punctuationPrettifier :: Punctuation -> Extension
+punctuationPrettifier Punctuation {..} = Ext.inlineTrans $ \case
+  Plain txt -> Plain
+    . f punctEnDash (T.replace "--"  "–")
+    . f punctEmDash (T.replace "---" "—")
+    $ txt
+  other -> other
+  where
+    f b g = if b then g else id
+
+----------------------------------------------------------------------------
+-- Email address obfuscation
+
+-- | This extension makes email addresses in links be rendered as something
+-- like this:
+--
+-- > <a class="protected-email"
+-- >    data-email="something@example.org"
+-- >    href="javascript:void(0)">Enable JavaScript to see the email</a>
+--
+-- You'll also need to include jQuery and this bit of JS code for the magic
+-- to work:
+--
+-- > $(document).ready(function () {
+-- >     $(".protected-email").each(function () {
+-- >         var item = $(this);
+-- >         var email = item.data('email');
+-- >         item.attr('href', 'mailto:' + email);
+-- >         item.html(email);
+-- >     });
+-- > });
+
+obfuscateEmail
+  :: Text
+     -- ^ Name of class to assign to the links, e.g. @\"protected-email\"@
+  -> Extension
+obfuscateEmail class' = Ext.inlineRender $ \old inline ->
+  case inline of
+    l@(Link _ email mtitle) ->
+      if URI.uriScheme email == URI.mkScheme "mailto"
+        then let txt = Plain "Enable JavaScript to see the email" :| []
+                 js  = fromJust (URI.mkURI "javascript:void(0)")
+             in with (old (Link txt js mtitle))
+                  [ class_ class'
+                  , data_ "email"
+                    (URI.render email { URI.uriScheme = Nothing }) ]
+        else old l
+    other -> old other
+
+----------------------------------------------------------------------------
+-- Font Awesome icons
+
+-- | Allow to insert @span@s with font awesome icons using autolinks like
+-- this:
+--
+-- > <fa:user>
+--
+-- This @user@ identifier is the name of icon you want to insert. You can
+-- also control the size of the icon like this:
+--
+-- > <fa:user/fw> -- fixed width
+-- > <fa:user/lg> -- large
+-- > <fa:user/2x>
+-- > <fa:user/3x>
+-- > <fa:user/4x>
+-- > <fa:user/5x>
+--
+-- In general, all path components in this URI that go after the name of
+-- icon will be prefixed with @\"fa-\"@ and added as classes, so you can do
+-- a lot of fancy stuff, see <http://fontawesome.io/examples/>:
+--
+-- > <fa:quote-left/3x/pull-left/border>
+--
+-- See also: <http://fontawesome.io>.
+
+fontAwesome :: Extension
+fontAwesome = Ext.inlineRender $ \old inline ->
+  case inline of
+    l@(Link _ fa _) ->
+      if URI.uriScheme fa == URI.mkScheme "fa"
+        then case URI.uriPath fa of
+               [] -> old l
+               xs ->
+                 let g x = "fa-" <> URI.unRText x
+                 in span_
+                    [ (class_ . T.intercalate " ") ("fa" : fmap g xs) ]
+                    ""
+        else old l
+    other -> old other
diff --git a/data/toc.html b/data/toc.html
new file mode 100644
--- /dev/null
+++ b/data/toc.html
@@ -0,0 +1,155 @@
+<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>
+<ul>
+<li><a href="#nullam-luctus">Nullam luctus</a><ul>
+<li><a href="#sed-euismod">Sed euismod</a><ul>
+<li><a href="#curabitur-ullamcorper">Curabitur ullamcorper</a><ul>
+<li><a href="#ut-dictum-quis">Ut dictum quis</a><ul>
+<li><a href="#mauris-efficitur">Mauris efficitur</a></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+<li><a href="#aenean-auctor-nec">Aenean auctor nec</a></li>
+</ul>
+<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>
+<h3 id="sed-euismod">Sed euismod</h3>
+<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>
+<h4 id="curabitur-ullamcorper">Curabitur ullamcorper</h4>
+<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>
+<h5 id="ut-dictum-quis">Ut dictum quis</h5>
+<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>
+<h6 id="mauris-efficitur">Mauris efficitur</h6>
+<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/toc.md b/data/toc.md
new file mode 100644
--- /dev/null
+++ b/data/toc.md
@@ -0,0 +1,167 @@
+# 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.
+
+```toc
+```
+
+## 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-ext.cabal b/mmark-ext.cabal
new file mode 100644
--- /dev/null
+++ b/mmark-ext.cabal
@@ -0,0 +1,62 @@
+name:                 mmark-ext
+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-ext
+bug-reports:          https://github.com/mrkkrp/mmark-ext/issues
+category:             Text
+synopsis:             Commonly useful extensions for MMark markdown processor
+build-type:           Simple
+description:          Commonly useful extensions for MMark markdown processor.
+extra-doc-files:      CHANGELOG.md
+                    , README.md
+data-files:           data/*.md
+                    , data/*.html
+
+source-repository head
+  type:               git
+  location:           https://github.com/mrkkrp/mmark-ext.git
+
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
+
+library
+  build-depends:      base             >= 4.8   && < 5.0
+                    , data-default-class
+                    , foldl            >= 1.2   && < 1.4
+                    , lucid            >= 2.6   && < 3.0
+                    , mmark            >= 0.0.1 && <= 0.1
+                    , modern-uri       >= 0.1.1 && < 0.2
+                    , text             >= 0.2   && < 1.3
+  if !impl(ghc >= 8.0)
+    build-depends:    semigroups       == 0.18.*
+  exposed-modules:    Text.MMark.Extension.Common
+  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:      base             >= 4.8   && < 5.0
+                    , data-default-class
+                    , hspec            >= 2.0   && < 3.0
+                    , lucid            >= 2.6   && < 3.0
+                    , mmark            >= 0.0.1 && <= 0.1
+                    , mmark-ext
+                    , text             >= 0.2   && < 1.3
+  if flag(dev)
+    ghc-options:      -O0 -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  other-modules:      Text.MMark.Extension.CommonSpec
+  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/Extension/CommonSpec.hs b/tests/Text/MMark/Extension/CommonSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Text/MMark/Extension/CommonSpec.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.MMark.Extension.CommonSpec (spec) where
+
+import Data.Default.Class
+import Data.Text (Text)
+import Test.Hspec
+import qualified Data.Text.IO                as TIO
+import qualified Data.Text.Lazy              as TL
+import qualified Lucid                       as L
+import qualified Text.MMark                  as MMark
+import qualified Text.MMark.Extension.Common as Ext
+
+spec :: Spec
+spec = parallel $ do
+  describe "toc" $
+    it "works" $
+      "data/toc.md" `withToc` "data/toc.html"
+  describe "punctuationPrettifier" $ do
+    let to = withExt (Ext.punctuationPrettifier def)
+        ot = withExt $ Ext.punctuationPrettifier def
+          { Ext.punctEnDash = False
+          , Ext.punctEmDash = False }
+    context "on plain inlines" $ do
+      context "when enabeled" $ do
+        it "replaces -- with en dash" $
+          "Here we go -- at last." `to` "<p>Here we go – at last.</p>\n"
+        it "replaces --- with em dash" $
+          "Here we go---at last." `to` "<p>Here we go—at last.</p>\n"
+      context "when disabled" $ do
+        it "does not replace -- with en dash" $
+          "Here we go -- at last." `ot` "<p>Here we go -- at last.</p>\n"
+        it "does not replace --- with em dash" $
+          "Here we go---at last." `ot` "<p>Here we go---at last.</p>\n"
+    context "on other inlines" $
+      it "has no effect" $
+        "`code -- span`" `to` "<p><code>code -- span</code></p>\n"
+  describe "obfuscateEmail" $ do
+    let to = withExt (Ext.obfuscateEmail "foo")
+    context "when URI has the mailto scheme" $
+      it "produces the correct HTML" $
+        "<mailto:me@example.org>" `to` "<p><a href=\"javascript:void(0)\" class=\"foo\" data-email=\"me@example.org\">Enable JavaScript to see the email</a></p>\n"
+    context "when URI has some other scheme" $
+      it "produces the correct HTML" $
+        "<https:example.org>" `to` "<p><a href=\"https:example.org\">https:example.org</a></p>\n"
+    context "other elements" $
+      it "not affected" $
+        "Something." `to` "<p>Something.</p>\n"
+  describe "fontAwesome" $ do
+    let to = withExt Ext.fontAwesome
+    context "when URI has the fa scheme" $
+      it "produces the correct HTML" $ do
+        "<fa:>" `to` "<p><a href=\"fa:\">fa:</a></p>\n"
+        "<fa:user>" `to` "<p><span class=\"fa fa-user\"></span></p>\n"
+        "<fa:user/lg>" `to` "<p><span class=\"fa fa-user fa-lg\"></span></p>\n"
+        "<fa:quote-left/3x/pull-left/border>" `to` "<p><span class=\"fa fa-quote-left fa-3x fa-pull-left fa-border\"></span></p>\n"
+    context "when URI has some other scheme" $
+      it "produces the correct HTML" $
+        "<https://example.org>" `to` "<p><a href=\"https://example.org/\">https://example.org/</a></p>\n"
+    context "other elements" $
+      it "not affected" $
+        "Something." `to` "<p>Something.</p>\n"
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Feed input into MMark parser, apply an extension, render the parsed
+-- document and demand that it matches the given example.
+
+withExt
+  :: MMark.Extension   -- ^ MMark extension to use
+  -> Text              -- ^ Input for the parser
+  -> Text              -- ^ Expected output of the render
+  -> Expectation
+withExt ext input expected = do
+  let Right doc = MMark.parse "" input
+      actual = TL.toStrict
+        . L.renderText
+        . MMark.render
+        . MMark.useExtension ext
+        $ doc
+
+  actual `shouldBe` expected
+-- | Similar to 'withExt' but specialized to test the 'Ext.toc' extension
+-- and loads input and expected output from files.
+
+withToc
+  :: FilePath          -- ^ File containing input for the parser
+  -> FilePath          -- ^ File containing expected output of the render
+  -> Expectation
+withToc ipath opath = do
+  input    <- TIO.readFile ipath
+  expected <- TIO.readFile opath
+  let Right doc = MMark.parse "" input
+      toc       = MMark.runScanner doc (Ext.tocScanner 6)
+      actual = TL.toStrict
+        . L.renderText
+        . MMark.render
+        . MMark.useExtension (Ext.toc "toc" toc)
+        $ doc
+  actual `shouldBe` expected
