diff --git a/app/speed.hs b/app/speed.hs
deleted file mode 100644
--- a/app/speed.hs
+++ /dev/null
@@ -1,204 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# OPTIONS_GHC -Wno-unused-do-bind #-}
-
--- | basic measurement and callibration
-module Main where
-
-import Data.ByteString qualified as B
-import Data.Text.IO qualified as Text
-import FlatParse.Basic (byteStringOf, char, satisfy, skipMany)
-import FlatParse.Basic qualified as FP
-import MarkupParse
-import MarkupParse.FlatParse
-import Options.Applicative as OA
-import Perf
-import Text.HTML.Parser qualified as HP
-import Text.HTML.Tree qualified as HP
-import Prelude
-
-data RunType = RunDefault | RunReduced | RunMarkup | RunWhitespace | RunWrappedQ | RunIsa | RunByteStringOf deriving (Eq, Show)
-
-data SpeedOptions = SpeedOptions
-  { speedReportOptions :: ReportOptions,
-    speedRunType :: RunType,
-    speedFile :: FilePath,
-    speedSnippet :: B.ByteString
-  }
-  deriving (Eq, Show)
-
-parseRun :: OA.Parser RunType
-parseRun =
-  flag' RunDefault (long "default" <> help "run default performance test")
-    <|> flag' RunMarkup (long "markup" <> short 'm' <> help "run markup performance test")
-    <|> flag' RunWhitespace (long "whitespace" <> help "run whitespace parsing test")
-    <|> flag' RunWrappedQ (long "wrappedQ" <> help "run wrappedQ parsing test")
-    <|> flag' RunReduced (long "reduced" <> help "run with reduced result sizes")
-    <|> flag' RunByteStringOf (long "bytestringof" <> help "test bytestringof")
-    <|> flag' RunIsa (long "isa" <> help "test isa")
-    <|> pure RunDefault
-
-speedOptions :: OA.Parser SpeedOptions
-speedOptions =
-  SpeedOptions
-    <$> parseReportOptions
-    <*> parseRun
-    <*> strOption (value "other/line.svg" <> long "file" <> short 'f' <> help "file to test")
-    <*> strOption (value "'wrapped'" <> long "snippet" <> help "snippet to parse")
-
-speedInfo :: ParserInfo SpeedOptions
-speedInfo =
-  info
-    (speedOptions <**> helper)
-    (fullDesc <> progDesc "markup-parse benchmarking" <> header "speed tests")
-
-main :: IO ()
-main = do
-  o <- execParser speedInfo
-  let rep = speedReportOptions o
-  let r = speedRunType o
-  let f = speedFile o
-  let snip = speedSnippet o
-
-  case r of
-    RunDefault -> do
-      bs <- B.readFile f
-      t <- Text.readFile f
-
-      reportMainWith rep (show r) $ do
-        ts' <- ffap "html-parse tokens" HP.parseTokens t
-        _ <- ffap "html-parse tree" (either undefined id . HP.tokensToForest) ts'
-        tsHtml <-
-          warnError
-            <$> ffap "tokenize" (tokenize Xml) bs
-        _ <-
-          warnError
-            <$> ffap "gather" (gather Xml) tsHtml
-        m <-
-          warnError
-            <$> ffap "markup" (markup Xml) bs
-        _ <- ffap "normalize" normalize m
-        _ <- ffap "markdown" (markdown Compact Xml) m
-        pure ()
-    RunMarkup -> do
-      bs <- B.readFile f
-      reportMainWith rep (show r) $ do
-        fap "markup" (length . elements . markup_ Xml) bs
-    RunWhitespace -> do
-      reportMainWith rep (show r) (wsFap " \n\nx")
-    RunWrappedQ -> do
-      reportMainWith rep (show r) (fapWrappedQ snip)
-    RunIsa -> do
-      reportMainWith rep (show r) fapIsa
-    RunByteStringOf -> do
-      reportMainWith rep (show r) fapBSOf
-    RunReduced -> do
-      bs <- B.readFile f
-      t <- Text.readFile f
-      let ts' = HP.parseTokens t
-      let ts = tokenize_ Xml bs
-      let m = markup_ Xml bs
-      reportMainWith rep (show r) $ do
-        _ <- ffap "html-parse tokens" (length . HP.parseTokens) t
-        _ <- ffap "html-parse tree" (either undefined length . HP.tokensToForest) ts'
-        _ <- ffap "tokenize" (length . tokenize Xml) bs
-        _ <- ffap "gather" (length . elements . gather_ Xml) ts
-        _ <- ffap "markup" (length . elements . markup_ Xml) bs
-        _ <- ffap "normalize" normalize m
-        _ <- ffap "markdown" (markdown Compact Xml) m
-        pure ()
-
--- | Consume whitespace.
-wsSwitch_ :: FP.Parser e ()
-wsSwitch_ =
-  $( FP.switch
-       [|
-         case _ of
-           " " -> wsSwitch_
-           "\n" -> wsSwitch_
-           "\t" -> wsSwitch_
-           "\r" -> wsSwitch_
-           "\f" -> wsSwitch_
-           _ -> pure ()
-         |]
-   )
-
--- | consume whitespace
-wsSatisfy_ :: FP.Parser e ()
-wsSatisfy_ = FP.skipMany (FP.satisfy isWhitespace)
-
--- | consume whitespace
-wsFusedSatisfy_ :: FP.Parser e ()
-wsFusedSatisfy_ = FP.skipMany (FP.fusedSatisfy isWhitespace (const False) (const False) (const False)) >> pure ()
-
--- | consume whitespace
-wsSatisfyAscii_ :: FP.Parser e ()
-wsSatisfyAscii_ = FP.skipMany (FP.satisfyAscii isWhitespace) >> pure ()
-
-wsFap :: B.ByteString -> PerfT IO [[Double]] ()
-wsFap bs = do
-  fap "wsFusedSatisfy_" (FP.runParser wsFusedSatisfy_) bs
-  fap "ws" (FP.runParser ws) bs
-  fap "wss" (FP.runParser wss) bs
-  fap "ws_" (FP.runParser ws_) bs
-  fap "wsSwitch_" (FP.runParser wsSwitch_) bs
-  fap "wsSatisfy_" (FP.runParser wsSatisfy_) bs
-  fap "wsSatisfyAscii_" (FP.runParser wsSatisfyAscii_) bs
-  pure ()
-
-fapWrappedQ :: B.ByteString -> PerfT IO [[Double]] ()
-fapWrappedQ bs = do
-  fap "wrappedQ" (FP.runParser wrappedQ) bs
-  fap "wrappedQSatisfy" (FP.runParser wrappedQSatisfy) bs
-  fap "wrappedQSkipSatisfy" (FP.runParser wrappedQSkipSatisfy) bs
-  fap "wrappedQNotA" (FP.runParser wrappedQNotA) bs
-  fap "wrappedQCandidate" (FP.runParser wrappedQCandidate) bs
-  pure ()
-
-fapIsa :: PerfT IO [[Double]] ()
-fapIsa = do
-  fap "isa isAttrName" (FP.runParser (isa isAttrName)) "name"
-  fap "attrNameP" (FP.runParser attrNameP) "name"
-  pure ()
-
-fapBSOf :: PerfT IO [[Double]] ()
-fapBSOf = do
-  fap "byteStringOf" (FP.runParser (byteStringOf (attrsP Html))) " a=\"a\" b=b c>"
-  fap "byteStringOf'" (FP.runParser (byteStringOf' (attrsP Html))) " a=\"a\" b=b c>"
-  pure ()
-
-isAttrName :: Char -> Bool
-isAttrName x =
-  not $
-    isWhitespace x
-      || (x == '/')
-      || (x == '>')
-      || (x == '=')
-
-attrNameP :: FP.Parser e B.ByteString
-attrNameP = byteStringOf $ some (satisfy isAttrName)
-
-wrappedQSatisfy :: FP.Parser e B.ByteString
-wrappedQSatisfy =
-  ($(char '"') *> (byteStringOf $ many (satisfy (/= '"'))) <* $(char '"'))
-    <|> ($(char '\'') *> (byteStringOf $ many (satisfy (/= '\''))) <* $(char '\''))
-
-wrappedQSkipSatisfy :: FP.Parser e B.ByteString
-wrappedQSkipSatisfy =
-  ($(char '"') *> (byteStringOf $ skipMany (satisfy (/= '"'))) <* $(char '"'))
-    <|> ($(char '\'') *> (byteStringOf $ skipMany (satisfy (/= '\''))) <* $(char '\''))
-
-wrappedQNotA :: FP.Parser e B.ByteString
-wrappedQNotA =
-  ($(char '"') *> nota '"' <* $(char '"'))
-    <|> ($(char '\'') *> nota '\'' <* $(char '\''))
-
-wrappedQCandidate :: FP.Parser e B.ByteString
-wrappedQCandidate =
-  wrappedSQ' <|> wrappedDQ'
-
-wrappedSQ' :: FP.Parser b B.ByteString
-wrappedSQ' = $(char '\'') *> nota '\'' <* $(char '\'')
-
-wrappedDQ' :: FP.Parser b B.ByteString
-wrappedDQ' = $(char '"') *> nota '"' <* $(char '"')
diff --git a/markup-parse.cabal b/markup-parse.cabal
--- a/markup-parse.cabal
+++ b/markup-parse.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: markup-parse
-version: 0.1.1
+version: 0.1.1.1
 license: BSD-3-Clause
 license-file: LICENSE
 copyright: Copyright, Tony Day, 2023-
@@ -13,12 +13,15 @@
 description:
     A markup parser and printer, from and to strict bytestrings, optimised for speed.
 build-type: Simple
-tested-with: GHC == 9.6.2
+tested-with:
+    , GHC == 9.10.1
+    , GHC == 9.6.5
+    , GHC == 9.8.2
 extra-doc-files:
     ChangeLog.md
     other/*.html
     other/*.svg
-    readme.org
+    readme.md
 
 source-repository head
     type: git
@@ -40,69 +43,16 @@
         -Wredundant-constraints
 
 common ghc2021-stanza
-    if impl ( ghc >= 9.2 )
-        default-language: GHC2021
-
-    if impl ( ghc < 9.2 )
-        default-language: Haskell2010
-        default-extensions:
-            BangPatterns
-            BinaryLiterals
-            ConstrainedClassMethods
-            ConstraintKinds
-            DeriveDataTypeable
-            DeriveFoldable
-            DeriveFunctor
-            DeriveGeneric
-            DeriveLift
-            DeriveTraversable
-            DoAndIfThenElse
-            EmptyCase
-            EmptyDataDecls
-            EmptyDataDeriving
-            ExistentialQuantification
-            ExplicitForAll
-            FlexibleContexts
-            FlexibleInstances
-            ForeignFunctionInterface
-            GADTSyntax
-            GeneralisedNewtypeDeriving
-            HexFloatLiterals
-            ImplicitPrelude
-            InstanceSigs
-            KindSignatures
-            MonomorphismRestriction
-            MultiParamTypeClasses
-            NamedFieldPuns
-            NamedWildCards
-            NumericUnderscores
-            PatternGuards
-            PolyKinds
-            PostfixOperators
-            RankNTypes
-            RelaxedPolyRec
-            ScopedTypeVariables
-            StandaloneDeriving
-            StarIsType
-            TraditionalRecordSyntax
-            TupleSections
-            TypeApplications
-            TypeOperators
-            TypeSynonymInstances
-
-    if impl ( ghc < 9.2 ) && impl ( ghc >= 8.10 )
-        default-extensions:
-            ImportQualifiedPost
-            StandaloneKindSignatures
+    default-language: GHC2021
 
 library
     import: ghc-options-stanza
     import: ghc2021-stanza
     hs-source-dirs: src
     build-depends:
-        , base               >=4.7 && <5
+        , base               >=4.14 && <5
         , bytestring         >=0.11.3 && <0.13
-        , containers         >=0.6 && <0.7
+        , containers         >=0.6 && <0.8
         , deepseq            >=1.4.4 && <1.6
         , flatparse          >=0.3.5 && <0.6
         , string-interpolate >=0.3 && <0.4
@@ -115,6 +65,16 @@
         MarkupParse.FlatParse
         MarkupParse.Patch
 
+test-suite doctests
+    import: ghc2021-stanza
+    main-is: doctests.hs
+    hs-source-dirs: test
+    build-depends:
+        , base             >=4.14 && <5
+        , doctest-parallel >=0.3 && <0.4
+    ghc-options: -threaded
+    type: exitcode-stdio-1.0
+
 test-suite markup-parse-diff
     import: ghc-options-exe-stanza
     import: ghc-options-stanza
@@ -122,29 +82,11 @@
     main-is: diff.hs
     hs-source-dirs: app
     build-depends:
-        , base               >=4.7 && <5
+        , base               >=4.14 && <5
         , bytestring         >=0.11.3 && <0.13
         , markup-parse
         , string-interpolate >=0.3 && <0.4
-        , tasty              >=1.2 && <1.5
+        , tasty              >=1.2 && <1.6
         , tasty-golden       >=2.3.1.1 && <2.4
         , tree-diff          >=0.3 && <0.4
-    type: exitcode-stdio-1.0
-
-benchmark markup-parse-speed
-    import: ghc-options-exe-stanza
-    import: ghc-options-stanza
-    import: ghc2021-stanza
-    main-is: speed.hs
-    hs-source-dirs: app
-    build-depends:
-        , base                 >=4.7 && <5
-        , bytestring           >=0.11.3 && <0.13
-        , flatparse            >=0.3.5 && <0.6
-        , html-parse           >=0.2 && <0.3
-        , markup-parse
-        , optparse-applicative >=0.17 && <0.19
-        , perf                 >=0.12 && <0.13
-        , text                 >=1.2 && <2.2
-    ghc-options: -O2
     type: exitcode-stdio-1.0
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,210 @@
+
+# Table of Contents
+
+1.  [markup-parse](#org813b686)
+2.  [Development](#org3d7b96c)
+3.  [Main Pipeline types](#orgb8f20ca)
+4.  [MarkupParse.Patch](#org3c16287)
+5.  [wiki diff test debug](#org25edc96)
+6.  [Reference](#org4a018da)
+    1.  [Prior Art](#org1ed6bdf)
+7.  [Performance](#org9b10723)
+    1.  [Profiling](#orgf660933)
+
+
+<a id="org813b686"></a>
+
+# markup-parse
+
+[![img](https://img.shields.io/hackage/v/markup-parse.svg)](https://hackage.haskell.org/package/markup-parse)
+[![img](https://github.com/tonyday567/markup-parse/workflows/haskell-ci/badge.svg)](https://github.com/tonyday567/markup-parse/actions?query=workflow%3Ahaskell-ci)
+
+`markup-parse` parses and prints a subset of common XML & HTML structured data, from and to strict bytestrings
+
+
+<a id="org3d7b96c"></a>
+
+# Development
+
+    :r
+    :set prompt "> "
+    :set -Wno-type-defaults
+    :set -Wno-name-shadowing
+    :set -XOverloadedStrings
+    :set -XTemplateHaskell
+    :set -XQuasiQuotes
+    import Control.Monad
+    import MarkupParse
+    import MarkupParse.FlatParse
+    import MarkupParse.Patch
+    import Data.ByteString qualified as B
+    import Data.ByteString.Char8 qualified as C
+    import Data.Map.Strict qualified as Map
+    import Data.Function
+    import FlatParse.Basic hiding (take)
+    import Data.String.Interpolate
+    import Data.TreeDiff
+    import Control.Monad
+    bs <- B.readFile "other/line.svg"
+    C.length bs
+
+    Preprocessing library for markup-parse-0.1.1..
+    GHCi, version 9.6.2: https://www.haskell.org/ghc/  :? for help
+    [1 of 3] Compiling MarkupParse.FlatParse ( src/MarkupParse/FlatParse.hs, interpreted )
+    [2 of 3] Compiling MarkupParse      ( src/MarkupParse.hs, interpreted )
+    [3 of 3] Compiling MarkupParse.Patch ( src/MarkupParse/Patch.hs, interpreted )
+    Ok, three modules loaded.
+    g
+    Ok, three modules loaded.
+    7554
+
+
+<a id="orgb8f20ca"></a>
+
+# Main Pipeline types
+
+    :t tokenize Html
+    :t gather Html
+    :t normalize
+    :t degather
+    :t detokenize Html
+    :t tokenize Html >=> gather Html >=> (normalize >>> pure) >=> degather Html >=> (fmap (detokenize Html) >>> pure)
+
+    tokenize Html :: ByteString -> Warn [Token]
+    gather Html :: [Token] -> Warn Markup
+    normalize :: Markup -> Markup
+    degather :: Standard -> Markup -> Warn [Token]
+    detokenize Html :: Token -> ByteString
+    tokenize Html >=> gather Html >=> (normalize >>> pure) >=> degather Html >=> (fmap (detokenize Html) >>> pure)
+      :: ByteString -> These [MarkupWarning] [ByteString]
+
+Round trip equality
+
+    m = markup_ Xml bs
+    m == (markup_ Xml $ markdown_ Compact Xml m)
+
+    True
+
+
+<a id="org3c16287"></a>
+
+# MarkupParse.Patch
+
+Obviously, patch doesn&rsquo;t belong here long-term but has been very useful in testing and development.
+
+    showPatch $ patch [1, 2, 3, 5] [0, 1, 2, 4, 6]
+
+    [+0, -3, +4, -5, +6]
+
+
+<a id="org25edc96"></a>
+
+# wiki diff test debug
+
+    bs <- B.readFile "other/Parsing - Wikipedia.html"
+    m = markup_ Html bs
+    m == (markup_ Html $ markdown_ Compact Html m)
+
+    True
+
+
+<a id="org4a018da"></a>
+
+# Reference
+
+[HTML Standard](https://html.spec.whatwg.org/#toc-syntax)
+
+[Are (non-void) self-closing tags valid in HTML5? - Stack Overflow](https://stackoverflow.com/questions/3558119/are-non-void-self-closing-tags-valid-in-html5)
+
+[Extensible Markup Language (XML) 1.0 (Fifth Edition)](https://www.w3.org/TR/xml/)
+
+
+<a id="org1ed6bdf"></a>
+
+## Prior Art
+
+[html-parse](https://hackage.haskell.org/package/html-parse) is an attoparsec-based parser for HTML. The HTML parsing here has referenced this parsing logic for HTML elements.
+
+[blaze-markup](https://hackage.haskell.org/package/blaze-markup) & [lucid](https://hackage.haskell.org/package/lucid) are HTML DSLs and printers, but not parsers.
+
+[xeno](https://hackage.haskell.org/package/xeno) is an &ldquo;event-based&rdquo; XML parser with a more complicated base markup type.
+
+[XMLParser](https://hackage.haskell.org/package/XMLParser) & [hexml](https://hackage.haskell.org/package/hexml) are parsec-based parsers.
+
+
+<a id="org9b10723"></a>
+
+# Performance
+
+The [perf](https://hackage.haskell.org/package/perf) library has been used to measure performance of the library.
+
+\`cabal bench\` runs the default benchmarking:
+
+    cabal bench
+
+    Running 1 benchmarks...
+    Benchmark markup-parse-speed: RUNNING...
+    label1          label2          old result      new result      change
+    
+    gather          time            9.33e4          7.73e4          improvement
+    html-parse tokenstime            1.21e6          1.17e6
+    html-parse tree time            6.61e4          7.59e4          slightly-degraded
+    markdown        time            3.75e5          3.92e5
+    markup          time            4.72e5          5.20e5          slightly-degraded
+    normalize       time            2.80e5          3.04e5          slightly-degraded
+    tokenize        time            8.59e5          8.67e5
+    Benchmark markup-parse-speed: FINISH
+
+
+<a id="orgf660933"></a>
+
+## Profiling
+
+Profiling is used to aid library development:
+
+    cabal configure --enable-library-profiling --enable-executable-profiling -fprof-auto -fprof --write-ghc-environment-files=always --enable-benchmarks -O2
+
+cabal.project.local
+
+    write-ghc-environment-files: always
+    ignore-project: False
+    flags: +prof +prof-auto
+    library-profiling: True
+    executable-profiling: True
+
+Profiling slow the main functions significantly:
+
+    ./app/speed -n 1000 --best -c +RTS -s -p -hc -l -RTS
+    label1              label2              old_result          new_result          status
+    
+    gather              time                2.08e4              3.01e4              degraded
+    html-parse tokens   time                4.70e5              1.72e6              degraded
+    html-parse tree     time                2.30e4              3.85e4              degraded
+    markdown            time                3.51e5              5.70e5              degraded
+    markup              time                2.10e5              1.05e6              degraded
+    normalize           time                8.43e4              1.90e5              degraded
+    tokenize            time                1.94e5              1.02e6              degraded
+       4,520,989,296 bytes allocated in the heap
+       2,668,887,592 bytes copied during GC
+         287,122,272 bytes maximum residency (21 sample(s))
+           1,572,000 bytes maximum slop
+                 560 MiB total memory in use (0 MiB lost due to fragmentation)
+    
+                                         Tot time (elapsed)  Avg pause  Max pause
+      Gen  0      1073 colls,     0 par    0.471s   0.479s     0.0004s    0.0024s
+      Gen  1        21 colls,     0 par    2.428s   2.575s     0.1226s    0.3303s
+    
+      INIT    time    0.007s  (  0.008s elapsed)
+      MUT     time    2.142s  (  1.945s elapsed)
+      GC      time    1.904s  (  2.071s elapsed)
+      RP      time    0.000s  (  0.000s elapsed)
+      PROF    time    0.995s  (  0.982s elapsed)
+      EXIT    time    0.026s  (  0.000s elapsed)
+      Total   time    5.074s  (  5.006s elapsed)
+    
+      %GC     time       0.0%  (0.0% elapsed)
+    
+      Alloc rate    2,110,654,040 bytes per MUT second
+    
+      Productivity  61.8% of total user, 58.5% of total elapsed
+
diff --git a/readme.org b/readme.org
deleted file mode 100644
--- a/readme.org
+++ /dev/null
@@ -1,196 +0,0 @@
-* markup-parse
-
-[[https://hackage.haskell.org/package/markup-parse][https://img.shields.io/hackage/v/markup-parse.svg]]
-[[https://github.com/tonyday567/markup-parse/actions?query=workflow%3Ahaskell-ci][https://github.com/tonyday567/markup-parse/workflows/haskell-ci/badge.svg]]
-
-~markup-parse~ parses and prints a subset of common XML & HTML structured data, from and to strict bytestrings
-
-* Development
-
-#+begin_src haskell :results output :exports both
-:r
-:set prompt "> "
-:set -Wno-type-defaults
-:set -Wno-name-shadowing
-:set -XOverloadedStrings
-:set -XTemplateHaskell
-:set -XQuasiQuotes
-import Control.Monad
-import MarkupParse
-import MarkupParse.FlatParse
-import MarkupParse.Patch
-import Data.ByteString qualified as B
-import Data.ByteString.Char8 qualified as C
-import Data.Map.Strict qualified as Map
-import Data.Function
-import FlatParse.Basic hiding (take)
-import Data.String.Interpolate
-import Data.TreeDiff
-import Control.Monad
-bs <- B.readFile "other/line.svg"
-C.length bs
-#+end_src
-
-#+RESULTS:
-: Preprocessing library for markup-parse-0.1.1..
-: GHCi, version 9.6.2: https://www.haskell.org/ghc/  :? for help
-: [1 of 3] Compiling MarkupParse.FlatParse ( src/MarkupParse/FlatParse.hs, interpreted )
-: [2 of 3] Compiling MarkupParse      ( src/MarkupParse.hs, interpreted )
-: [3 of 3] Compiling MarkupParse.Patch ( src/MarkupParse/Patch.hs, interpreted )
-: Ok, three modules loaded.
-: g
-: Ok, three modules loaded.
-: 7554
-
-* Main Pipeline types
-
-#+begin_src haskell :results output :exports both
-:t tokenize Html
-:t gather Html
-:t normalize
-:t degather
-:t detokenize Html
-:t tokenize Html >=> gather Html >=> (normalize >>> pure) >=> degather Html >=> (fmap (detokenize Html) >>> pure)
-#+end_src
-
-#+RESULTS:
-: tokenize Html :: ByteString -> Warn [Token]
-: gather Html :: [Token] -> Warn Markup
-: normalize :: Markup -> Markup
-: degather :: Standard -> Markup -> Warn [Token]
-: detokenize Html :: Token -> ByteString
-: tokenize Html >=> gather Html >=> (normalize >>> pure) >=> degather Html >=> (fmap (detokenize Html) >>> pure)
-:   :: ByteString -> These [MarkupWarning] [ByteString]
-
-Round trip equality
-
-#+begin_src haskell :results output :exports both
-m = markup_ Xml bs
-m == (markup_ Xml $ markdown_ Compact Xml m)
-#+end_src
-
-#+RESULTS:
-: True
-
-* MarkupParse.Patch
-
-Obviously, patch doesn't belong here long-term but has been very useful in testing and development.
-
-#+begin_src haskell :results output :exports both
-showPatch $ patch [1, 2, 3, 5] [0, 1, 2, 4, 6]
-#+end_src
-
-#+RESULTS:
-: [+0, -3, +4, -5, +6]
-
-* wiki diff test debug
-
-#+begin_src haskell :results output :exports both
-bs <- B.readFile "other/Parsing - Wikipedia.html"
-m = markup_ Html bs
-m == (markup_ Html $ markdown_ Compact Html m)
-#+end_src
-
-#+RESULTS:
-: True
-
-* Reference
-
-[[https://html.spec.whatwg.org/#toc-syntax][HTML Standard]]
-
-[[https://stackoverflow.com/questions/3558119/are-non-void-self-closing-tags-valid-in-html5][Are (non-void) self-closing tags valid in HTML5? - Stack Overflow]]
-
-[[https://www.w3.org/TR/xml/][Extensible Markup Language (XML) 1.0 (Fifth Edition)]]
-
-** Prior Art
-
-[[https://hackage.haskell.org/package/html-parse][html-parse]] is an attoparsec-based parser for HTML. The HTML parsing here has referenced this parsing logic for HTML elements.
-
-[[https://hackage.haskell.org/package/blaze-markup][blaze-markup]] & [[https://hackage.haskell.org/package/lucid][lucid]] are HTML DSLs and printers, but not parsers.
-
-[[https://hackage.haskell.org/package/xeno][xeno]] is an "event-based" XML parser with a more complicated base markup type.
-
-[[https://hackage.haskell.org/package/XMLParser][XMLParser]] & [[https://hackage.haskell.org/package/hexml][hexml]] are parsec-based parsers.
-
-* Performance
-
-The [[https://hackage.haskell.org/package/perf][perf]] library has been used to measure performance of the library.
-
-`cabal bench` runs the default benchmarking:
-
-#+begin_src sh :results output :exports both
-cabal bench
-#+end_src
-
-#+RESULTS:
-#+begin_example
-Running 1 benchmarks...
-Benchmark markup-parse-speed: RUNNING...
-label1          label2          old result      new result      change
-
-gather          time            9.33e4          7.73e4          improvement
-html-parse tokenstime            1.21e6          1.17e6
-html-parse tree time            6.61e4          7.59e4          slightly-degraded
-markdown        time            3.75e5          3.92e5
-markup          time            4.72e5          5.20e5          slightly-degraded
-normalize       time            2.80e5          3.04e5          slightly-degraded
-tokenize        time            8.59e5          8.67e5
-Benchmark markup-parse-speed: FINISH
-#+end_example
-
-** Profiling
-
-Profiling is used to aid library development:
-
-#+begin_src sh :results output
-cabal configure --enable-library-profiling --enable-executable-profiling -fprof-auto -fprof --write-ghc-environment-files=always --enable-benchmarks -O2
-#+end_src
-
-cabal.project.local
-
-#+begin_example
-write-ghc-environment-files: always
-ignore-project: False
-flags: +prof +prof-auto
-library-profiling: True
-executable-profiling: True
-#+end_example
-
-Profiling slow the main functions significantly:
-
-#+begin_example
-./app/speed -n 1000 --best -c +RTS -s -p -hc -l -RTS
-label1              label2              old_result          new_result          status
-
-gather              time                2.08e4              3.01e4              degraded
-html-parse tokens   time                4.70e5              1.72e6              degraded
-html-parse tree     time                2.30e4              3.85e4              degraded
-markdown            time                3.51e5              5.70e5              degraded
-markup              time                2.10e5              1.05e6              degraded
-normalize           time                8.43e4              1.90e5              degraded
-tokenize            time                1.94e5              1.02e6              degraded
-   4,520,989,296 bytes allocated in the heap
-   2,668,887,592 bytes copied during GC
-     287,122,272 bytes maximum residency (21 sample(s))
-       1,572,000 bytes maximum slop
-             560 MiB total memory in use (0 MiB lost due to fragmentation)
-
-                                     Tot time (elapsed)  Avg pause  Max pause
-  Gen  0      1073 colls,     0 par    0.471s   0.479s     0.0004s    0.0024s
-  Gen  1        21 colls,     0 par    2.428s   2.575s     0.1226s    0.3303s
-
-  INIT    time    0.007s  (  0.008s elapsed)
-  MUT     time    2.142s  (  1.945s elapsed)
-  GC      time    1.904s  (  2.071s elapsed)
-  RP      time    0.000s  (  0.000s elapsed)
-  PROF    time    0.995s  (  0.982s elapsed)
-  EXIT    time    0.026s  (  0.000s elapsed)
-  Total   time    5.074s  (  5.006s elapsed)
-
-  %GC     time       0.0%  (0.0% elapsed)
-
-  Alloc rate    2,110,654,040 bytes per MUT second
-
-  Productivity  61.8% of total user, 58.5% of total elapsed
-#+end_example
-
diff --git a/src/MarkupParse.hs b/src/MarkupParse.hs
--- a/src/MarkupParse.hs
+++ b/src/MarkupParse.hs
@@ -457,7 +457,7 @@
 -- Markup {elements = [Node {rootLabel = Content "<content>", subForest = []}]}
 --
 -- >>> markup_ Html $ markdown_ Compact Html $ contentRaw "<content>"
--- *** Exception: UnclosedTag
+-- Markup {elements = *** Exception: UnclosedTag
 -- ...
 contentRaw :: ByteString -> Markup
 contentRaw bs = Markup [pure $ Content bs]
diff --git a/src/MarkupParse/FlatParse.hs b/src/MarkupParse/FlatParse.hs
--- a/src/MarkupParse/FlatParse.hs
+++ b/src/MarkupParse/FlatParse.hs
@@ -57,6 +57,7 @@
 
 -- $setup
 -- >>> :set -XTemplateHaskell
+-- >>> :set -XOverloadedStrings
 -- >>> import MarkupParse.FlatParse
 -- >>> import FlatParse.Basic
 
diff --git a/test/doctests.hs b/test/doctests.hs
new file mode 100644
--- /dev/null
+++ b/test/doctests.hs
@@ -0,0 +1,6 @@
+import System.Environment (getArgs)
+import Test.DocTest (mainFromCabal)
+import Prelude (IO, (=<<))
+
+main :: IO ()
+main = mainFromCabal "markup-parse" =<< getArgs
