diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,31 @@
+# `literatex-haskell` Changelog
+
+This project follows the [Haskell package versioning policy][PVP], with
+versions in `A.B.C.D` format.  `A` may be incremented arbitrarily for
+non-technical reasons, but [semantic versioning][SemVer] is otherwise
+followed, where `A.B` is the major version, `C` is the minor version, and `D`
+is the patch version.  Initial development uses versions `0.0.0.D`, for which
+every version is considered breaking.
+
+[PVP]: <https://pvp.haskell.org/>
+[SemVer]: <https://semver.org/>
+
+The format of this changelog is based on [Keep a Changelog][KaC], with the
+following conventions:
+
+* Level-two heading `Unreleased` is used to track changes that have not been
+  released.
+* Other level-two headings specify the release in `A.B.C.D (YYYY-MM-DD)`
+  format, with newer versions above older versions.
+* Level-three headings are used to categorize changes as follows:
+    1. Breaking
+    2. Non-Breaking
+* Changes are listed in arbitrary order and present tense.
+
+[KaC]: <https://keepachangelog.com/en/1.0.0/>
+
+## 0.1.0.0 (2021-05-26)
+
+### Breaking
+
+* Initial public release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2021 Travis Cardwell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,366 @@
+# LiterateX
+
+[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
+[![GitHub CI](https://github.com/ExtremaIS/literatex-haskell/workflows/CI/badge.svg?branch=main)](https://github.com/ExtremaIS/literatex-haskell/actions)
+[![Hackage](https://img.shields.io/hackage/v/literatex.svg)](https://hackage.haskell.org/package/literatex)
+[![Stackage LTS](https://stackage.org/package/literatex/badge/lts)](https://stackage.org/package/literatex)
+[![Stackage Nightly](https://stackage.org/package/literatex/badge/nightly)](https://stackage.org/nightly/package/literatex)
+
+![LiterateX: LiterateBash, LiterateC, LiterateClojure, LiterateErlang, LiterateHaskell, LiterateIdris, LiterateJavaScript, LiteratePython, LiterateRacket, LiterateSQL, Literate...](project/animation/literatex.gif)
+
+* [Overview](#overview)
+* [Source Formats](#source-formats)
+    * [Double-Dash Comments](#double-dash-comments)
+    * [Double-Slash Comments](#double-slash-comments)
+    * [Hash Comments](#hash-comments)
+    * [Lisp Semicolon Comments](#lisp-semicolon-comments)
+    * [Literate Haskell](#literate-haskell)
+    * [Percent Comments](#percent-comments)
+* [CLI](#cli)
+    * [Requirements](#requirements)
+    * [Installation](#installation)
+        * [Installation From Source](#installation-from-source)
+        * [`.deb` Package Installation](#deb-package-installation)
+        * [`.rpm` Package Installation](#rpm-package-installation)
+    * [Usage](#usage)
+* [Library](#library)
+* [Related Work](#related-work)
+* [Project](#project)
+    * [Links](#links)
+    * [Releases](#releases)
+    * [Contribution](#contribution)
+    * [License](#license)
+
+## Overview
+
+LiterateX transforms literate source code to Markdown.  Write documentation in
+Markdown format in the comments of source code, and LiterateX can transform
+the file to Markdown, optionally including the source code with syntax
+highlighting and line numbers.  Many [source formats](#source-formats) are
+supported, and the following target formats are supported:
+
+* [Pandoc Markdown](https://pandoc.org/MANUAL.html#pandocs-markdown)
+* [GitHub Flavored Markdown](https://github.github.com/gfm/)
+
+LiterateX can be used to document code that is particularly important or
+difficult to understand.  For example, documentation can be written in the SQL
+file that defines the schema for a complex database.  LiterateX can translate
+the `.sql` file to a Markdown file, which can then be converted to HTML, PDF,
+or even EPUB with [Pandoc][].
+
+[Pandoc]: <https://pandoc.org/>
+
+LiterateX can also be used in the publishing of blog entries, magazine
+articles, or books.  Use the [command-line utility](#cli) or integrate the
+Haskell [library](#library) into your own software.
+
+LiterateX has support for **source code rules**, comment lines that are used
+to visually separate sections of source code.  Since source code rules are
+only used to make the source code easier to scan quickly, they are ignored
+(treated as a blank line) when translating to Markdown.
+
+LiterateX also has support for [shebang][] lines at the start of the file.
+They can be ignored so that they do not precede the documentation.
+
+[shebang]: <https://en.wikipedia.org/wiki/Shebang_(Unix)>
+
+## Source Formats
+
+LiterateX supports a number of source formats.  With the exception of
+[literate Haskell](#literate-haskell), documentation is written in line
+comments in the source language.  Note that multi-line comments are treated as
+code, not documentation.
+
+### Double-Dash Comments
+
+Documentation is parsed from lines that begin with two dashes immediately
+followed by a space (`-- `).  Lines that only contain two dashes are treated
+as blank lines in the documentation.  Lines that contain three or more dashes
+are treated as source code rules and are ignored.
+
+``` haskell
+-- # Haskell Example
+--
+-- Executables are implemented using a `Main` module that exposes a function
+-- named `main`.
+
+module Main (main) where
+
+-- The `main` function is run when the program is executed.
+
+main :: IO ()
+main = putStrLn "Hello!"
+
+-- This simple example just prints "Hello!" to the screen.
+```
+
+Languages that use double-dash comments include the following:
+
+* [Elm](https://elm-lang.org/)
+* [Haskell](https://www.haskell.org/)
+* [Idris](https://www.idris-lang.org/)
+* [Lua](http://www.lua.org/)
+* [SQL](https://en.wikipedia.org/wiki/SQL)
+
+### Double-Slash Comments
+
+Documentation is parsed from lines that begin with two slashes immediately
+followed by a space (`// `).  Lines that only contain two slashes are treated
+as blank lines in the documentation.  Lines that contain three or more slashes
+are treated as source code rules and are ignored.
+
+``` rust
+// # Rust Example
+//
+// The `main` function is run when the program is executed.
+
+fn main() {
+    println!("Hello!");
+}
+
+// This simple example just prints "Hello!" to the screen.
+```
+
+Languages that use double-slash comments include the following:
+
+* [C][]
+* [CSS](https://en.wikipedia.org/wiki/CSS)
+* [Go](https://golang.org/)
+* [Java](https://www.java.com/)
+* [JavaScript](https://en.wikipedia.org/wiki/JavaScript)
+* [Kotlin](https://kotlinlang.org/)
+* [PHP](https://www.php.net/)
+* [Rust](https://www.rust-lang.org/)
+* [Scala](https://www.scala-lang.org/)
+* [TypeScript](https://www.typescriptlang.org/)
+
+[C]: <https://en.wikipedia.org/wiki/C_(programming_language)>
+
+### Hash Comments
+
+Documentation is parsed from lines that begin with a hash character
+immediately followed by a space (`# `).  Lines that only contain a hash
+character are treated as blank lines in the documentation.  Lines that contain
+two or more hash characters are treated as source code rules and are ignored.
+
+``` python
+# # Python Example
+#
+# A quick-and-dirty Python script can include commands at the top level!
+
+print("Hello!")
+
+# This simple example just prints "Hello!" to the screen.
+```
+
+Languages that use hash comments include the following:
+
+* [Bash](https://www.gnu.org/software/bash/)
+* [Elixir](https://elixir-lang.org/)
+* [Perl](https://www.perl.org/)
+* [Python](https://www.python.org/)
+* [R](https://www.r-project.org/)
+* [Ruby](https://www.ruby-lang.org/)
+
+### Lisp Semicolon Comments
+
+Lisp languages use a semicolon (`;`) for line comments, but there is a
+special convention to use a different number of semicolons according to the
+context.  Documentation is parsed from lines that begin with one to four
+semicolons immediately followed by a space (`; `, `;; `, `;;; `, or `;;;; `).
+Lines that only contain one to four semicolons are treated as blank lines in
+the documentation.  Lines that contain more than four semicolons are treated
+as source code rules and are ignored.
+
+``` scheme
+; # Racket Example
+;
+; Racket programs must declare the language to use.
+
+#lang racket/base
+
+; Racket can also include commands at the top level!
+
+(println "Hello!")
+
+; This simple example just prints "Hello!" to the screen.
+```
+
+Languages that use Lisp semicolon comments include the following:
+
+* [Clojure](https://clojure.org/)
+* [Common Lisp](https://common-lisp.net/)
+* [Racket](https://racket-lang.org/)
+* [Scheme][]
+
+[Scheme]: <https://en.wikipedia.org/wiki/Scheme_(programming_language)>
+
+### Literate Haskell
+
+[GHC](https://www.haskell.org/ghc/) has special support for
+[literate programming](https://wiki.haskell.org/Literate_programming).  With
+this source format, documentation is not written in comments.  Instead, lines
+that contain source code are prefixes with a greater-than sign and a space
+(`> `).
+
+Note that this source format does not support source code rules.
+
+``` lhaskell
+# Literate Haskell Example
+
+Executables are implemented using a `Main` module that exposes a function
+named `main`.
+
+> module Main (main) where
+
+The `main` function is run when the program is executed.
+
+> main :: IO ()
+> main = putStrLn "Hello!"
+
+This simple example just prints "Hello!" to the screen.
+```
+
+### Percent Comments
+
+Documentation is parsed from lines that begin with a percent character
+immediately followed by a space (`% `).  Lines that only contain a percent
+character are treated as blank lines in the documentation.  Lines that contain
+two or more percent characters are treated as source code rules and are
+ignored.
+
+``` erlang
+% # Erlang Example
+%
+% Programs are implemented using a `main` module that exports a `start/0`
+% function.
+
+-module(main).
+-export([start/0]).
+
+% The `start` function is run when the program is executed.
+
+start() -> io.fwrite("Hello!\n").
+
+% This simple example just prints "Hello!" to the screen.
+```
+
+Languages that use percent comments include the following:
+
+* [Erlang](https://www.erlang.org/)
+* [LaTeX](https://www.latex-project.org/)
+
+## CLI
+
+LiterateX may be used via a command-line utility named `literatex`.
+
+### Requirements
+
+`literatex` has only been tested on Linux.  It *might* work on Windows and
+macOS.
+
+### Installation
+
+#### Installation From Source
+
+`literatex` can be built from source using [Stack][].  For example, you can
+install the latest release (to `/usr/local` on Linux) as follows:
+
+```
+$ git clone https://github.com/ExtremaIS/literatex-haskell.git
+$ cd literatex-haskell
+$ make
+$ sudo make install
+```
+
+[Stack]: <https://www.haskellstack.org>
+
+#### `.deb` Package Installation
+
+Check the [Releases][] page for `.deb` packages.
+
+#### `.rpm` Package Installation
+
+Check the [Releases][] page for `.rpm` packages.
+
+[Releases]: <https://github.com/ExtremaIS/literatex-haskell/releases>
+
+### Usage
+
+See the [`literatex` man page](doc/literatex.1.md) for usage information.
+
+## Library
+
+The [LiterateX Haskell library][] provides an API for integrating LiterateX
+functionality in your own software.
+
+[LiterateX Haskell library]: <https://hackage.haskell.org/package/literatex>
+
+## Related Work
+
+[Literate programming][] is a style of programming introduced by
+[Donald Knuth][] in which [the main idea][] is "to regard a program as a
+communication to human beings rather than as a set of instructions to a
+computer."  LiterateX is faithful to this idea in that it is used for
+communication to human beings.  Note, however, that LiterateX does *not*
+support another core aspect of Knuth's literate programming: the ability to
+write source code in the order best for human understanding.  Since LiterateX
+transforms actual source code files, the source code has to be written in
+whatever order is required by the language.  Those interested in writing code
+in different order are encouraged to check out [noweb][] and [CWEB][].
+
+[Literate programming]: <https://en.wikipedia.org/wiki/Literate_programming>
+[Donald Knuth]: <https://en.wikipedia.org/wiki/Donald_Knuth>
+[the main idea]: <https://www-cs-faculty.stanford.edu/~knuth/cweb.html>
+[noweb]: <https://en.wikipedia.org/wiki/Noweb>
+[CWEB]: <https://en.wikipedia.org/wiki/CWEB>
+
+The [lhs2tex][] utility is used to work with literate Haskell and LaTeX.
+
+[lhs2tex]: <https://github.com/kosmikus/lhs2tex>
+
+The [src2md][] utility, written in Common Lisp, also supports multiple source
+formats.  It outputs Markdown that includes HTML, which limits the usefulness
+of the Markdown.
+
+[src2md]: <https://git.sr.ht/~aerique/src2markup>
+
+The [extract-documentation-comments][] utility, written in JavaScript,
+extracts documentation from multi-line JavaScript comments.
+
+[extract-documentation-comments]: <https://github.com/Anadian/extract-documentation-comments#readme>
+
+[mlp.clj][], written in Clojure, is a [babashka][] script that transforms
+literate Clojure source code to Markdown, including HTML.  The author uses it
+to implement a [live preview][] of literate Clojure documentation while using
+the [Notepad++][] (Windows editor).
+
+[mlp.clj]: <https://github.com/linpengcheng/ClojureBoxNpp/blob/master/Notepad%2B%2B/tools/clj/mlp.clj>
+[babashka]: <https://github.com/babashka/babashka#readme>
+[live preview]: <https://github.com/linpengcheng/ClojureBoxNpp/tree/master/Notepad%2B%2B/plugins/Config/PreviewHTML>
+[Notepad++]: <https://notepad-plus-plus.org/>
+
+## Project
+
+### Links
+
+* GitHub: <https://github.com/ExtremaIS/literatex-haskell>
+
+### Releases
+
+All releases are tagged in the `main` branch.  Release tags are signed using
+the
+[`security@extrema.is` GPG key](http://keys.gnupg.net/pks/lookup?op=vindex&fingerprint=on&search=0x1D484E4B4705FADF).
+
+### Contribution
+
+Issues and feature requests are tracked on GitHub:
+<https://github.com/ExtremaIS/literatex-haskell/issues>
+
+Issues may also be submitted via email to <bugs@extrema.is>.
+
+### License
+
+This project is released under the
+[MIT License](https://opensource.org/licenses/MIT) as specified in the
+[`LICENSE`](LICENSE) file.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/LibOA.hs b/app/LibOA.hs
new file mode 100644
--- /dev/null
+++ b/app/LibOA.hs
@@ -0,0 +1,122 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LibOA
+-- Description : supplementary functions for optparse-applicative
+-- Copyright   : Copyright (c) 2019-2021 Travis Cardwell
+-- License     : MIT
+--
+-- This is a collection of functions that I often use with
+-- @optparse-applicative@.  I do not feel that it is worth maintaining yet
+-- another helper package on Hackage, so I just copy the code to different
+-- projects as required.  If the library grows to a substantial size or others
+-- with to use it, I will reconsider.
+--
+-- Revision: 2021-04-04
+------------------------------------------------------------------------------
+
+{-# LANGUAGE CPP #-}
+
+module LibOA
+  ( -- * Options
+    -- $Options
+    helper
+  , versioner
+    -- * Utilities
+  , commands
+    -- * Help
+  , (<||>)
+  , section
+  , table
+  , vspace
+  ) where
+
+-- https://hackage.haskell.org/package/ansi-wl-pprint
+import qualified Text.PrettyPrint.ANSI.Leijen as Doc
+import Text.PrettyPrint.ANSI.Leijen (Doc)
+
+-- https://hackage.haskell.org/package/base
+import qualified Data.List as List
+#if !MIN_VERSION_base (4,11,0)
+import Data.Monoid ((<>))
+#endif
+
+-- https://hackage.haskell.org/package/optparse-applicative
+import qualified Options.Applicative as OA
+import qualified Options.Applicative.Common as OAC
+import qualified Options.Applicative.Types as OAT
+
+------------------------------------------------------------------------------
+-- $Options
+--
+-- Option descriptions are not capitalized.
+
+-- | A hidden @-h@ / @--help@ option that always fails, showing the help
+--
+-- This is the same as 'OA.helper' except that it has a different help
+-- message.
+helper :: OA.Parser (a -> a)
+#if MIN_VERSION_optparse_applicative (0,16,0)
+helper = OA.option helpReader $ mconcat
+    [ OA.short 'h'
+    , OA.long "help"
+    , OA.help "show this help text"
+    , OA.hidden
+    ]
+  where
+    helpReader = do
+      potentialCommand <- OAT.readerAsk
+      OA.readerAbort $ OA.ShowHelpText (Just potentialCommand)
+#else
+helper = OA.abortOption OA.ShowHelpText $ mconcat
+    [ OA.short 'h'
+    , OA.long "help"
+    , OA.help "show help and exit"
+    , OA.hidden
+    ]
+#endif
+
+-- | A hidden @--version@ option that always fails, showing the version
+versioner
+  :: String  -- ^ version string
+  -> OA.Parser (a -> a)
+versioner verStr = OA.infoOption verStr $ mconcat
+    [ OA.long "version"
+    , OA.help "show version and exit"
+    , OA.hidden
+    ]
+
+------------------------------------------------------------------------------
+-- $Utilities
+
+-- | Get a list of commands for a parser
+commands :: OA.Parser a -> [String]
+commands =
+    let go _ opt = case OAT.optMain opt of
+           OAT.CmdReader _ cmds _ -> reverse cmds
+           _otherReader           -> []
+    in  concat . OAC.mapParser go
+
+------------------------------------------------------------------------------
+-- $Help
+
+-- | Insert a blank line between two documents
+(<||>) :: Doc -> Doc -> Doc
+d1 <||> d2 = d1 <> Doc.line <> Doc.line <> d2
+infixr 5 <||>
+
+-- | Create a section with a title and indented body
+section :: String -> Doc -> Doc
+section title = (Doc.text title Doc.<$$>) . Doc.indent 2
+
+-- | Create a two-column table
+table :: [(String, String)] -> Doc
+table rows =
+    let width = 1 + maximum (map (length . fst) rows)
+    in  Doc.vcat
+          [ Doc.fillBreak width (Doc.text l) Doc.<+> Doc.text r
+          | (l, r) <- rows
+          ]
+
+-- | Vertically space documents with blank lines between them
+vspace :: [Doc] -> Doc
+vspace = mconcat . List.intersperse (Doc.line <> Doc.line)
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,210 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : Main
+-- Description : literatex command-line utility
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+------------------------------------------------------------------------------
+
+{-# LANGUAGE RecordWildCards #-}
+
+module Main (main) where
+
+-- https://hackage.haskell.org/package/ansi-wl-pprint
+import Text.PrettyPrint.ANSI.Leijen (Doc)
+
+-- https://hackage.haskell.org/package/base
+import Control.Applicative (optional)
+import System.Exit (ExitCode(ExitFailure), exitWith)
+import System.IO (hPutStrLn, stderr, stdin, stdout)
+
+-- https://hackage.haskell.org/package/optparse-applicative
+import qualified Options.Applicative as OA
+
+-- https://hackage.haskell.org/package/ttc
+import qualified Data.TTC as TTC
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.SourceDefaults as SourceDefaults
+import LiterateX.Types (CodeLanguage, SourceFormat, TargetFormat)
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+-- (literatex:executable)
+import qualified LibOA
+
+------------------------------------------------------------------------------
+-- $Defaults
+
+defaultTargetFormat :: TargetFormat
+defaultTargetFormat = TargetFormat.PandocMarkdown
+
+------------------------------------------------------------------------------
+-- $Options
+
+data Options
+  = Options
+    { formatOpt      :: !(Maybe SourceFormat)
+    , rendererOpts   :: !Renderer.Options
+    , noHighlightOpt :: !Bool
+    , inputOpt       :: !(Maybe FilePath)
+    , outputOpt      :: !(Maybe FilePath)
+    }
+
+sourceFormatOption :: OA.Parser SourceFormat
+sourceFormatOption = OA.option (OA.eitherReader TTC.parse) $ mconcat
+    [ OA.long "source"
+    , OA.short 's'
+    , OA.metavar "SOURCE"
+    , OA.help "source format"
+    ]
+
+targetFormatOption :: OA.Parser TargetFormat
+targetFormatOption = OA.option (OA.eitherReader TTC.parse) $ mconcat
+    [ OA.long "target"
+    , OA.short 't'
+    , OA.metavar "TARGET"
+    , OA.value defaultTargetFormat
+    , OA.showDefaultWith TTC.render
+    , OA.help "target format"
+    ]
+
+languageOption :: OA.Parser CodeLanguage
+languageOption = OA.option (OA.eitherReader TTC.parse) $ mconcat
+    [ OA.long "language"
+    , OA.short 'l'
+    , OA.metavar "LANGUAGE"
+    , OA.help "code language"
+    ]
+
+ignoreShebangOption :: OA.Parser Bool
+ignoreShebangOption = OA.switch $ mconcat
+    [ OA.long "ignore-shebang"
+    , OA.help "ignore shebangs"
+    ]
+
+noCodeOption :: OA.Parser Bool
+noCodeOption = OA.switch $ mconcat
+    [ OA.long "no-code"
+    , OA.help "do not display code"
+    ]
+
+noNumbersOption :: OA.Parser Bool
+noNumbersOption = OA.switch $ mconcat
+    [ OA.long "no-numbers"
+    , OA.help "do not number code lines"
+    ]
+
+renderOptions :: OA.Parser Renderer.Options
+renderOptions = Renderer.Options
+    <$> targetFormatOption
+    <*> optional languageOption
+    <*> ignoreShebangOption
+    <*> fmap not noCodeOption
+    <*> fmap not noNumbersOption
+
+noHighlightOption :: OA.Parser Bool
+noHighlightOption = OA.switch $ mconcat
+    [ OA.long "no-highlight"
+    , OA.help "do not highlight code"
+    ]
+
+inputOption :: OA.Parser FilePath
+inputOption = OA.strOption $ mconcat
+    [ OA.long "input"
+    , OA.short 'i'
+    , OA.metavar "FILE"
+    , OA.help "input file (default: STDIN)"
+    ]
+
+outputOption :: OA.Parser FilePath
+outputOption = OA.strOption $ mconcat
+    [ OA.long "output"
+    , OA.short 'o'
+    , OA.metavar "FILE"
+    , OA.help "output file (default: STDOUT)"
+    ]
+
+options :: OA.Parser Options
+options = Options
+    <$> optional sourceFormatOption
+    <*> renderOptions
+    <*> noHighlightOption
+    <*> optional inputOption
+    <*> optional outputOption
+
+------------------------------------------------------------------------------
+-- $Library
+
+errorExit :: String -> IO a
+errorExit message = do
+    hPutStrLn stderr $ "error: " ++ message
+    exitWith $ ExitFailure 1
+
+------------------------------------------------------------------------------
+-- $Main
+
+main :: IO ()
+main = do
+    Options{..} <- OA.execParser pinfo
+    let defaultOpts = SourceDefaults.defaultsFor =<< inputOpt
+    format <- case (formatOpt, defaultOpts, inputOpt) of
+      (Just format, _, _) -> pure format
+      (Nothing, Just (format, _), _) -> pure format
+      (Nothing, Nothing, Just{}) ->
+        errorExit "source format not specified and unknown input filename"
+      (Nothing, Nothing, Nothing) ->
+        errorExit "source format not specified and no input filename"
+    let codeLanguage = Renderer.codeLanguage rendererOpts
+        opts = rendererOpts
+          { Renderer.codeLanguage =
+              case (noHighlightOpt, codeLanguage, defaultOpts) of
+                (True, _, _) -> Nothing
+                (False, opt@Just{}, _) -> opt
+                (False, Nothing, Just (_, lang)) -> Just lang
+                (False, Nothing, Nothing) -> Nothing
+          }
+    case (inputOpt, outputOpt) of
+      (Just input, Just output) ->
+        LiterateX.transformFileToFile format opts input output
+      (Just input, Nothing) ->
+        LiterateX.transformFileToHandle format opts input stdout
+      (Nothing, Just output) ->
+        LiterateX.transformHandleToFile format opts stdin output
+      (Nothing, Nothing) ->
+        LiterateX.transformHandleToHandle format opts stdin stdout
+  where
+    pinfo :: OA.ParserInfo Options
+    pinfo
+      = OA.info
+          (LibOA.helper <*> LibOA.versioner LiterateX.version <*> options)
+      $ mconcat
+          [ OA.fullDesc
+          , OA.progDesc "transform literate source code to Markdown"
+          , OA.failureCode 2
+          , OA.footerDoc . Just $ LibOA.vspace
+              [ sourceFormatHelp
+              , targetFormatHelp
+              , defaultsHelp
+              ]
+          ]
+
+    sourceFormatHelp :: Doc
+    sourceFormatHelp = LibOA.section "SOURCE options:" $ LibOA.table
+      [ (TTC.render format, SourceFormat.describe format)
+      | format <- SourceFormat.list
+      ]
+
+    targetFormatHelp :: Doc
+    targetFormatHelp = LibOA.section "TARGET options:" $ LibOA.table
+      [ (TTC.render format, TargetFormat.describe format)
+      | format <- TargetFormat.list
+      ]
+
+    defaultsHelp :: Doc
+    defaultsHelp = LibOA.section "Default options:" $ LibOA.table
+      [ (ext, TTC.render lang ++ " (" ++ TTC.render format ++ ")")
+      | (ext, (format, lang)) <- SourceDefaults.extensionDefaults
+      ]
diff --git a/examples/highlevel.lhs b/examples/highlevel.lhs
new file mode 100644
--- /dev/null
+++ b/examples/highlevel.lhs
@@ -0,0 +1,41 @@
+LiterateX High-Level Example
+============================
+
+This is a simple example of using the [LiterateX][] high-level API to
+transform Haskell source code to [Pandoc Markdown][].
+
+[LiterateX]: <https://github.com/ExtremaIS/literatex-haskell#readme>
+[Pandoc Markdown]: <https://pandoc.org/MANUAL.html#pandocs-markdown>
+
+Code
+----
+
+When executed, this example program reads this source code and transforms it
+to Markdown, writing the output to `STDOUT`.  Note that the path is
+hard-coded, so be sure to execute the program from the project directory.
+
+The `OverloadedStrings` extension is used to set the code language below.
+
+> {-# LANGUAGE OverloadedStrings #-}
+>
+> module Main (main) where
+
+Imports from [base](https://hackage.haskell.org/package/base):
+
+> import System.IO (stdout)
+
+Imports from [literatex](https://hackage.haskell.org/package/literatex):
+
+> import qualified LiterateX
+> import qualified LiterateX.Renderer as Renderer
+> import qualified LiterateX.Types.SourceFormat as SourceFormat
+
+The `main` function is just a call to `LiterateX.transformFileToHandle` with
+appropriate options.
+
+> main :: IO ()
+> main = LiterateX.transformFileToHandle
+>     SourceFormat.LiterateHaskell
+>     (Renderer.defaultOptionsFor "haskell")
+>     "examples/highlevel.lhs"
+>     stdout
diff --git a/examples/lowlevel.hs b/examples/lowlevel.hs
new file mode 100644
--- /dev/null
+++ b/examples/lowlevel.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+-- https://hackage.haskell.org/package/base
+import System.IO (stdout)
+
+-- https://hackage.haskell.org/package/literatex
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+
+------------------------------------------------------------------------------
+
+demo :: String
+demo = unlines
+    [ "-- Demo"
+    , "-- ===="
+    , "--"
+    , "-- This source code is embedded in the example, but it could come from"
+    , "-- a database or API call."
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+------------------------------------------------------------------------------
+
+main :: IO ()
+main = LiterateX.runIO
+    SourceFormat.DoubleDash
+    (Renderer.defaultOptionsFor "haskell")
+    (LiterateX.sourceString demo)
+    (LiterateX.sinkHandle stdout)
diff --git a/literatex.cabal b/literatex.cabal
new file mode 100644
--- /dev/null
+++ b/literatex.cabal
@@ -0,0 +1,138 @@
+name:           literatex
+version:        0.1.0.0
+category:       Utils
+synopsis:       transform literate source code to Markdown
+description:
+  This package provides a library as well as a command-line utility that
+  transforms literate source code to Markdown.  Please see the README on
+  GitHub at <https://github.com/ExtremaIS/literatex-haskell#readme>.
+
+homepage:       https://github.com/ExtremaIS/literatex-haskell#readme
+bug-reports:    https://github.com/ExtremaIS/literatex-haskell/issues
+author:         Travis Cardwell <travis.cardwell@extrema.is>
+maintainer:     Travis Cardwell <travis.cardwell@extrema.is>
+copyright:      Copyright (c) 2021 Travis Cardwell
+license:        MIT
+license-file:   LICENSE
+
+cabal-version:  1.24
+build-type:     Simple
+tested-with:
+  GHC ==8.2.2
+   || ==8.4.4
+   || ==8.6.5
+   || ==8.8.4
+   || ==8.10.4
+   || ==9.0.1
+
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  type: git
+  location: https://github.com/ExtremaIS/literatex-haskell.git
+
+flag examples
+  description: build examples
+  default: False
+
+flag write-hie
+  description: write .hie files
+  default: False
+
+library
+  hs-source-dirs: src
+  exposed-modules:
+      LiterateX
+    , LiterateX.Parser
+    , LiterateX.Renderer
+    , LiterateX.SourceDefaults
+    , LiterateX.Types
+    , LiterateX.Types.CodeLanguage
+    , LiterateX.Types.SourceFormat
+    , LiterateX.Types.SourceLine
+    , LiterateX.Types.TargetFormat
+  other-modules:
+      Paths_literatex
+  build-depends:
+      base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.12
+    , conduit >=1.3 && <1.4
+    , text >=1.2.3 && <1.3
+    , ttc >=0.4 && <0.5
+    , unliftio >=0.2 && <0.3
+  default-language: Haskell2010
+  default-extensions:
+      OverloadedStrings
+  if flag(write-hie)
+    ghc-options: -Wall -fwrite-ide-info -hiedir=.hie
+  else
+    ghc-options: -Wall
+
+executable literatex
+  hs-source-dirs: app
+  main-is: Main.hs
+  other-modules:
+      LibOA
+  build-depends:
+      ansi-wl-pprint >=0.6 && <0.7
+    , base
+    , optparse-applicative >=0.14 && <0.17
+    , literatex
+    , ttc
+  default-language: Haskell2010
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+
+executable example-highlevel
+  if flag(examples)
+    buildable: True
+  else
+    buildable: False
+  hs-source-dirs: examples
+  main-is: highlevel.lhs
+  build-depends:
+      base
+    , literatex
+  default-language: Haskell2010
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+
+executable example-lowlevel
+  if flag(examples)
+    buildable: True
+  else
+    buildable: False
+  hs-source-dirs: examples
+  main-is: lowlevel.hs
+  build-depends:
+      base
+    , literatex
+  default-language: Haskell2010
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+
+test-suite literatex-test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: Spec.hs
+  other-modules:
+      LiterateX.Test.API
+    , LiterateX.Test.SourceFormat
+    , LiterateX.Test.SourceFormat.DoubleDash
+    , LiterateX.Test.SourceFormat.DoubleSlash
+    , LiterateX.Test.SourceFormat.Hash
+    , LiterateX.Test.SourceFormat.LispSemicolons
+    , LiterateX.Test.SourceFormat.LiterateHaskell
+    , LiterateX.Test.SourceFormat.Percent
+    , LiterateX.Test.TargetFormat
+  build-depends:
+      base
+    , bytestring
+    , filepath
+    , literatex
+    , tasty
+    , tasty-hunit
+    , text
+    , ttc
+    , unliftio
+  default-language: Haskell2010
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
diff --git a/src/LiterateX.hs b/src/LiterateX.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX.hs
@@ -0,0 +1,531 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX
+-- Description : API
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+--
+-- This module provides high-level as well as low-level API functions for
+-- transforming literate source code.
+------------------------------------------------------------------------------
+
+{-# LANGUAGE FlexibleInstances #-}
+
+module LiterateX
+  ( -- * Constants
+    version
+    -- * API
+    -- ** High-Level
+    -- $HighLevelAPI
+  , transformTextToText
+  , transformTextToHandle
+  , transformTextToFile
+  , transformHandleToText
+  , transformHandleToHandle
+  , transformHandleToFile
+  , transformFileToText
+  , transformFileToHandle
+  , transformFileToFile
+    -- ** Low-Level
+    -- $LowLevelAPI
+  , runPure
+  , runIO
+  , runResource
+    -- *** Producers
+  , sourceString
+  , sourceText
+  , sourceLazyText
+  , sourceByteString
+  , sourceLazyByteString
+  , sourceHandle
+  , sourceFile
+    -- *** Consumers
+  , sinkString
+  , sinkText
+  , sinkLazyText
+  , sinkByteString
+  , sinkLazyByteString
+  , sinkHandle
+  , sinkFile
+    -- *** Transformers
+  , transform
+  ) where
+
+-- https://hackage.haskell.org/package/base
+import Control.Monad.IO.Class (MonadIO)
+import Data.Version (showVersion)
+import System.IO (Handle)
+
+-- https://hackage.haskell.org/package/bytestring
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+
+-- https://hackage.haskell.org/package/conduit
+import qualified Conduit as C
+import Data.Conduit ((.|))
+import qualified Data.Conduit.Combinators as CC
+import qualified Data.Conduit.List as CL
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+import qualified Data.Text.Encoding.Error as TEE
+import qualified Data.Text.Lazy as TL
+
+-- https://hackage.haskell.org/package/unliftio
+import UnliftIO (MonadUnliftIO)
+
+-- (literatex)
+import qualified LiterateX.Parser as Parser
+import qualified LiterateX.Renderer as Renderer
+import LiterateX.Types (SourceFormat)
+
+-- (literatex:cabal)
+import qualified Paths_literatex as Project
+
+------------------------------------------------------------------------------
+-- $Constants
+
+-- | LiterateX version string (\"@literatex-haskell X.X.X@\")
+version :: String
+version = "literatex-haskell " ++ showVersion Project.version
+
+------------------------------------------------------------------------------
+-- $HighLevelAPI
+--
+-- This high-level API provides functions for transforming literate source
+-- code.  These functions provide support for transforming from/to lazy
+-- 'TL.Text', 'Handle's, and files.
+--
+-- Example usage:
+--
+-- @
+-- {-\# LANGUAGE OverloadedStrings \#-}
+--
+-- module Main (main) where
+--
+-- -- https://hackage.haskell.org/package/literatex
+-- import qualified LiterateX
+-- import qualified LiterateX.Renderer as Renderer
+-- import qualified LiterateX.Types.SourceFormat as SourceFormat
+--
+-- main :: IO ()
+-- main = LiterateX.transformFileToFile
+--     SourceFormat.LiterateHaskell
+--     (Renderer.defaultOptionsFor "haskell")
+--     "demo.lhs"
+--     "demo.md"
+-- @
+
+-- | Transform from lazy 'TL.Text' to lazy 'TL.Text'
+--
+-- @since 0.0.1.0
+transformTextToText
+  :: SourceFormat
+  -> Renderer.Options
+  -> TL.Text
+  -> TL.Text
+transformTextToText sourceFormat rendererOpts source =
+    runPure sourceFormat rendererOpts (sourceLazyText source) sinkLazyText
+
+-- | Transform from lazy 'TL.Text' to a 'Handle'
+--
+-- @since 0.0.1.0
+transformTextToHandle
+  :: MonadIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> TL.Text
+  -> Handle
+  -> m ()
+transformTextToHandle sourceFormat rendererOpts source target =
+    runIO sourceFormat rendererOpts
+      (sourceLazyText source)
+      (sinkHandle target)
+
+-- | Transform from lazy 'TL.Text' to a file
+--
+-- @since 0.0.1.0
+transformTextToFile
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> TL.Text
+  -> FilePath
+  -> m ()
+transformTextToFile sourceFormat rendererOpts source target =
+    runResource sourceFormat rendererOpts
+      (sourceLazyText source)
+      (sinkFile target)
+
+-- | Transform from a 'Handle' to lazy 'TL.Text'
+--
+-- @since 0.0.1.0
+transformHandleToText
+  :: MonadIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> Handle
+  -> m TL.Text
+transformHandleToText sourceFormat rendererOpts source =
+    runIO sourceFormat rendererOpts (sourceHandle source) sinkLazyText
+
+-- | Transform from a 'Handle' to a 'Handle'
+--
+-- @since 0.0.1.0
+transformHandleToHandle
+  :: MonadIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> Handle
+  -> Handle
+  -> m ()
+transformHandleToHandle sourceFormat rendererOpts source target =
+    runIO sourceFormat rendererOpts (sourceHandle source) (sinkHandle target)
+
+-- | Transform from a 'Handle' to a file
+--
+-- @since 0.0.1.0
+transformHandleToFile
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> Handle
+  -> FilePath
+  -> m ()
+transformHandleToFile sourceFormat rendererOpts source target =
+    runResource sourceFormat rendererOpts
+      (sourceHandle source)
+      (sinkFile target)
+
+-- | Transform from a file to lazy 'TL.Text'
+--
+-- @since 0.0.1.0
+transformFileToText
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> FilePath
+  -> m TL.Text
+transformFileToText sourceFormat rendererOpts source =
+    runResource sourceFormat rendererOpts (sourceFile source) sinkLazyText
+
+-- | Transform from a file to a 'Handle'
+--
+-- @since 0.0.1.0
+transformFileToHandle
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> FilePath
+  -> Handle
+  -> m ()
+transformFileToHandle sourceFormat rendererOpts source target =
+    runResource sourceFormat rendererOpts
+      (sourceFile source)
+      (sinkHandle target)
+
+-- | Transform from a file to a file
+--
+-- @since 0.0.1.0
+transformFileToFile
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> FilePath
+  -> FilePath
+  -> m ()
+transformFileToFile sourceFormat rendererOpts source target =
+    runResource sourceFormat rendererOpts
+      (sourceFile source)
+      (sinkFile target)
+
+------------------------------------------------------------------------------
+-- $LowLevelAPI
+--
+-- This low-level API provides more control over transforming literate source
+-- code, using "Conduit".  The 'transform' transformer implements the
+-- transformation, transforming lines of input 'T.Text' to lines of output
+-- 'T.Text'.  Various producers are provided to produce lines of input
+-- 'T.Text' from common sources, and various consumers are provided to consume
+-- lines of output 'T.Text' to common sinks.  These can be used separately if
+-- necessary, but some run functions are provided for common usage.
+--
+-- Example usage:
+--
+-- @
+-- {-\# LANGUAGE OverloadedStrings \#-}
+--
+-- module Main (main) where
+--
+-- -- https://hackage.haskell.org/package/base
+-- import System.IO (stdout)
+--
+-- -- https://hackage.haskell.org/package/literatex
+-- import qualified LiterateX
+-- import qualified LiterateX.Renderer as Renderer
+-- import qualified LiterateX.Types.SourceFormat as SourceFormat
+--
+-- main :: IO ()
+-- main = do
+--     let demoBS = "..."
+--     LiterateX.runIO
+--       SourceFormat.LiterateHaskell
+--       (Renderer.defaultOptionsFor "haskell")
+--       (LiterateX.sourceByteString demoBS)
+--       (LiterateX.sinkHandle stdout)
+-- @
+
+-- | Run a pure LiterateX transformation
+--
+-- This function works with the following input line producers:
+--
+-- * 'sourceString'
+-- * 'sourceText'
+-- * 'sourceLazyText'
+-- * 'sourceByteString'
+-- * 'sourceLazyByteString'
+--
+-- This function works with the following output line consumers:
+--
+-- * 'sinkString'
+-- * 'sinkText'
+-- * 'sinkLazyText'
+-- * 'sinkByteString'
+-- * 'sinkLazyByteString'
+--
+-- @since 0.0.1.0
+runPure
+  :: SourceFormat
+  -> Renderer.Options
+  -> C.ConduitT () T.Text C.Identity ()     -- ^ input line producer
+  -> C.ConduitT T.Text C.Void C.Identity r  -- ^ output line consumer
+  -> r
+runPure sourceFormat rendererOpts source sink = C.runConduitPure $
+    source .| transform sourceFormat rendererOpts .| sink
+
+-- | Run a LiterateX transformation using IO
+--
+-- This function works with the following input line producers:
+--
+-- * 'sourceString'
+-- * 'sourceText'
+-- * 'sourceLazyText'
+-- * 'sourceByteString'
+-- * 'sourceLazyByteString'
+-- * 'sourceHandle'
+--
+-- This function works with the following output line consumers:
+--
+-- * 'sinkString'
+-- * 'sinkText'
+-- * 'sinkLazyText'
+-- * 'sinkByteString'
+-- * 'sinkLazyByteString'
+-- * 'sinkHandle'
+--
+-- @since 0.0.1.0
+runIO
+  :: MonadIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> C.ConduitT () T.Text m ()     -- ^ input line producer
+  -> C.ConduitT T.Text C.Void m r  -- ^ output line consumer
+  -> m r
+runIO sourceFormat rendererOpts source sink = C.runConduit $
+    source .| transform sourceFormat rendererOpts .| sink
+
+-- | Run a LiterateX transformation using resource management
+--
+-- This function works with the following input line producers:
+--
+-- * 'sourceString'
+-- * 'sourceText'
+-- * 'sourceLazyText'
+-- * 'sourceByteString'
+-- * 'sourceLazyByteString'
+-- * 'sourceHandle'
+-- * 'sourceFile'
+--
+-- This function works with the following output line consumers:
+--
+-- * 'sinkString'
+-- * 'sinkText'
+-- * 'sinkLazyText'
+-- * 'sinkByteString'
+-- * 'sinkLazyByteString'
+-- * 'sinkHandle'
+-- * 'sinkFile'
+--
+-- @since 0.0.1.0
+runResource
+  :: MonadUnliftIO m
+  => SourceFormat
+  -> Renderer.Options
+  -> C.ConduitT () T.Text (C.ResourceT m) ()     -- ^ input line producer
+  -> C.ConduitT T.Text C.Void (C.ResourceT m) r  -- ^ output line consumer
+  -> m r
+runResource sourceFormat rendererOpts source sink = C.runConduitRes $
+    source .| transform sourceFormat rendererOpts .| sink
+
+------------------------------------------------------------------------------
+-- $Producers
+
+-- | Produce input lines from a 'String' source
+--
+-- @since 0.0.1.0
+sourceString
+  :: Monad m
+  => String
+  -> C.ConduitT i T.Text m ()
+sourceString = CC.yieldMany . map T.pack . lines
+
+-- | Produce input lines from a 'T.Text' source
+--
+-- @since 0.0.1.0
+sourceText
+  :: Monad m
+  => T.Text
+  -> C.ConduitT i T.Text m ()
+sourceText source = C.yield source .| CC.linesUnbounded
+
+-- | Produce input lines from a lazy 'TL.Text' source
+--
+-- @since 0.0.1.0
+sourceLazyText
+  :: Monad m
+  => TL.Text
+  -> C.ConduitT i T.Text m ()
+sourceLazyText source = CC.sourceLazy source .| CC.linesUnbounded
+
+-- | Produce input lines from a 'BS.ByteString' source
+--
+-- @since 0.0.1.0
+sourceByteString
+  :: Monad m
+  => BS.ByteString
+  -> C.ConduitT i T.Text m ()
+sourceByteString source =
+    C.yield source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient
+
+-- | Produce input lines from a lazy 'BS.ByteString' source
+--
+-- @since 0.0.1.0
+sourceLazyByteString
+  :: Monad m
+  => BSL.ByteString
+  -> C.ConduitT i T.Text m ()
+sourceLazyByteString source =
+    CC.sourceLazy source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient
+
+-- | Produce input lines from a 'Handle' source
+--
+-- @since 0.0.1.0
+sourceHandle
+  :: MonadIO m
+  => Handle
+  -> C.ConduitT i T.Text m ()
+sourceHandle source =
+    CC.sourceHandle source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient
+
+-- | Produce input lines from a file source
+--
+-- @since 0.0.1.0
+sourceFile
+  :: C.MonadResource m
+  => FilePath
+  -> C.ConduitT i T.Text m ()
+sourceFile source =
+    CC.sourceFile source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient
+
+------------------------------------------------------------------------------
+-- $Consumers
+
+-- | Consume output lines, returning a 'String'
+--
+-- @since 0.0.1.0
+sinkString
+  :: Monad m
+  => C.ConduitT T.Text o m String
+sinkString = fmap TL.unpack $ CC.unlines .| CC.sinkLazy
+
+-- | Consume output lines, returning 'T.Text'
+--
+-- @since 0.0.1.0
+sinkText
+  :: Monad m
+  => C.ConduitT T.Text o m T.Text
+sinkText = fmap TL.toStrict $ CC.unlines .| CC.sinkLazy
+
+-- | Consume output lines, returning lazy 'T.Text'
+--
+-- @since 0.0.1.0
+sinkLazyText
+  :: Monad m
+  => C.ConduitT T.Text o m TL.Text
+sinkLazyText = CC.unlines .| CC.sinkLazy
+
+-- | Consume output lines, returning a 'BS.ByteString'
+--
+-- @since 0.0.1.0
+sinkByteString
+  :: Monad m
+  => C.ConduitT T.Text o m BS.ByteString
+sinkByteString =
+    fmap BSL.toStrict $ CC.unlines .| CC.encodeUtf8 .| CC.sinkLazy
+
+-- | Consume output lines, returning a lazy 'BS.ByteString'
+--
+-- @since 0.0.1.0
+sinkLazyByteString
+  :: Monad m
+  => C.ConduitT T.Text o m BSL.ByteString
+sinkLazyByteString = CC.unlines .| CC.encodeUtf8 .| CC.sinkLazy
+
+-- | Consume output lines, writing to a 'Handle'
+--
+-- @since 0.0.1.0
+sinkHandle
+  :: MonadIO m
+  => Handle
+  -> C.ConduitT T.Text o m ()
+sinkHandle target = CC.encodeUtf8 .| CC.unlinesAscii .| CC.sinkHandle target
+
+-- | Consume output lines, writing to a file
+--
+-- @since 0.0.1.0
+sinkFile
+  :: C.MonadResource m
+  => FilePath
+  -> C.ConduitT T.Text o m ()
+sinkFile target = CC.encodeUtf8 .| CC.unlinesAscii .| CC.sinkFile target
+
+------------------------------------------------------------------------------
+-- $Transformers
+
+-- | Transform input lines to output lines
+--
+-- @since 0.0.1.0
+transform
+  :: Monad m
+  => SourceFormat
+  -> Renderer.Options
+  -> C.ConduitT T.Text T.Text m ()
+transform sourceFormat rendererOpts
+    = Parser.parse sourceFormat
+    .| CL.concatMapAccum (\x n -> (n + 1, [(n, x)])) 1
+    .| Renderer.render rendererOpts
+
+------------------------------------------------------------------------------
+-- $Internal
+
+-- | Decode UTF-8 'BS.ByteString' lines to 'T.Text'
+--
+-- Note that 'CC.decodeUtf8Lenient' decodes UTF-8 streams and therefore
+-- discards empty strings.  This version decodes UTF-8 lines and does not
+-- discard empty lines.
+decodeUtf8LinesLenient
+  :: Monad m
+  => C.ConduitT BS.ByteString T.Text m ()
+decodeUtf8LinesLenient =
+    C.awaitForever $ C.yield . TE.decodeUtf8With TEE.lenientDecode
diff --git a/src/LiterateX/Parser.hs b/src/LiterateX/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Parser.hs
@@ -0,0 +1,193 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Parser
+-- Description : source parser
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+--
+-- This module implements the source parser.
+------------------------------------------------------------------------------
+
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module LiterateX.Parser
+  ( -- * API
+    parse
+  ) where
+
+-- https://hackage.haskell.org/package/base
+import Control.Monad (guard)
+import Data.Maybe (fromMaybe)
+
+-- https://hackage.haskell.org/package/conduit
+import qualified Data.Conduit as C
+import Data.Conduit (ConduitT)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import Data.Text (Text)
+
+-- (literatex)
+import LiterateX.Types (SourceFormat, SourceLine)
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.SourceLine as SourceLine
+
+------------------------------------------------------------------------------
+-- $API
+
+-- | Create a "Conduit" transformer that parses the specified source format
+--
+-- The transformer consumes lines of the input and produces a 'SourceLine' for
+-- each line of input.
+--
+-- @since 0.0.1.0
+parse
+  :: Monad m
+  => SourceFormat
+  -> ConduitT Text SourceLine m ()
+parse = parseSourceLines . parserFunctionsFor
+
+------------------------------------------------------------------------------
+-- $Internal
+
+-- | Parser functions that determine how input is parsed
+data ParserFunctions
+  = ParserFunctions
+    { isCodeBlank :: !(Text -> Bool)
+    , isDocBlank  :: !(Text -> Bool)
+    , isRule      :: !(Text -> Bool)
+    , getDoc      :: !(Text -> Maybe Text)
+    , getCode     :: !(Text -> Text)
+    }
+
+------------------------------------------------------------------------------
+
+-- | Get the parser functions for the specified source format
+parserFunctionsFor :: SourceFormat -> ParserFunctions
+parserFunctionsFor = \case
+    SourceFormat.DoubleDash      -> lineCommentParserFunctions '-' 2
+    SourceFormat.DoubleSlash     -> lineCommentParserFunctions '/' 2
+    SourceFormat.Hash            -> lineCommentParserFunctions '#' 1
+    SourceFormat.LiterateHaskell -> literateHaskellParserFunctions
+    SourceFormat.Percent         -> lineCommentParserFunctions '%' 1
+    SourceFormat.LispSemicolons  -> lispCommentParserFunctions
+
+------------------------------------------------------------------------------
+
+-- | Get parser functions for source with line-based comments
+lineCommentParserFunctions
+  :: Char  -- ^ comment character
+  -> Int   -- ^ number of comment characters to create line comment
+  -> ParserFunctions
+lineCommentParserFunctions char count = ParserFunctions{..}
+  where
+    docBlank :: Text
+    docBlank = T.pack $ replicate count char
+
+    prefixLen :: Int
+    prefixLen = count + 1
+
+    prefix :: Text
+    prefix = T.pack $ replicate count char ++ " "
+
+    isCodeBlank :: Text -> Bool
+    isCodeBlank = T.null
+
+    isDocBlank :: Text -> Bool
+    isDocBlank = (== docBlank)
+
+    isRule :: Text -> Bool
+    isRule line = T.length line > count && T.all (== char) line
+
+    getDoc :: Text -> Maybe Text
+    getDoc line = do
+      let (linePrefix, lineSuffix) = T.splitAt prefixLen line
+      guard $ linePrefix == prefix
+      pure lineSuffix
+
+    getCode :: Text -> Text
+    getCode = id
+
+------------------------------------------------------------------------------
+
+-- | Get parser functions for source with Lisp-style comments
+--
+-- Lisp-style comments begin with one or more semicolons.
+lispCommentParserFunctions :: ParserFunctions
+lispCommentParserFunctions = ParserFunctions{..}
+  where
+    isCodeBlank :: Text -> Bool
+    isCodeBlank = T.null
+
+    isDocBlank :: Text -> Bool
+    isDocBlank line =
+      let len = T.length line
+      in  len >= 1 && len <= 4 && T.all (== ';') line
+
+    isRule :: Text -> Bool
+    isRule line = T.length line > 4 && T.all (== ';') line
+
+    getDoc :: Text -> Maybe Text
+    getDoc line = do
+      let (linePrefix, (sep, lineSuffix)) = T.splitAt 1 <$> T.breakOn " " line
+      guard $ not (T.null linePrefix) && T.all (== ';') linePrefix
+      guard $ sep == " "
+      pure lineSuffix
+
+    getCode :: Text -> Text
+    getCode = id
+
+------------------------------------------------------------------------------
+
+-- | Get parser functions for parsing literate Haskell
+literateHaskellParserFunctions :: ParserFunctions
+literateHaskellParserFunctions = ParserFunctions{..}
+  where
+    isCodeBlank :: Text -> Bool
+    isCodeBlank = (== ">")
+
+    isDocBlank :: Text -> Bool
+    isDocBlank = T.null
+
+    isRule :: Text -> Bool
+    isRule = const False
+
+    getDoc :: Text -> Maybe Text
+    getDoc line
+      | "> " `T.isPrefixOf` line = Nothing
+      | otherwise                = Just line
+
+    getCode :: Text -> Text
+    getCode line = fromMaybe line $ T.stripPrefix "> " line
+
+------------------------------------------------------------------------------
+
+-- | Create a "Conduit" transformer for the specified parser functions
+--
+-- This function produces a 'SourceLine' for each line of input.  A
+-- 'SourceLine.Shebang' can only be produced on the first line.  Note that the
+-- order that the parser functions are used is significant; the parser
+-- functions are written for this order.
+parseSourceLines
+  :: Monad m
+  => ParserFunctions
+  -> ConduitT Text SourceLine m ()
+parseSourceLines ParserFunctions{..} = do
+    mLine <- C.await
+    case mLine of
+      Just line -> do
+        C.yield $ if "#!" `T.isPrefixOf` line
+          then SourceLine.Shebang line
+          else parse' line
+        C.awaitForever $ C.yield . parse'
+      Nothing -> return ()
+  where
+    parse' :: Text -> SourceLine
+    parse' line
+      | isCodeBlank line = SourceLine.CodeBlank
+      | isDocBlank line  = SourceLine.DocBlank
+      | isRule line      = SourceLine.Rule
+      | otherwise        = case (getDoc line, getCode line) of
+          (Just doc, _code) -> SourceLine.Doc doc
+          (Nothing,  code)  -> SourceLine.Code code
diff --git a/src/LiterateX/Renderer.hs b/src/LiterateX/Renderer.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Renderer.hs
@@ -0,0 +1,216 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Renderer
+-- Description : target renderer
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+--
+-- This module implements the target renderer.
+------------------------------------------------------------------------------
+
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module LiterateX.Renderer
+  ( -- * Types
+    Options(..)
+    -- * API
+  , defaultOptions
+  , defaultOptionsFor
+  , render
+  ) where
+
+-- https://hackage.haskell.org/package/base
+import Control.Monad (replicateM_)
+
+-- https://hackage.haskell.org/package/conduit
+import qualified Data.Conduit as C
+import Data.Conduit (ConduitT)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import Data.Text (Text)
+
+-- (literatex)
+import LiterateX.Types (CodeLanguage, SourceLine, TargetFormat)
+import qualified LiterateX.Types.SourceLine as SourceLine
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+-- $Types
+
+-- | Renderer options determine how output is rendered
+--
+-- @since 0.0.1.0
+data Options
+  = Options
+    { targetFormat    :: !TargetFormat
+    , codeLanguage    :: !(Maybe CodeLanguage)
+    , ignoreShebang   :: !Bool  -- ^ 'True' to ignore shebangs
+    , renderCode      :: !Bool  -- ^ 'True' to render code
+    , numberCodeLines :: !Bool  -- ^ 'True' to number code lines
+    }
+  deriving Show
+
+------------------------------------------------------------------------------
+-- $API
+
+-- | Default options
+--
+-- * @targetFormat@: 'TargetFormat.PandocMarkdown'
+-- * @codeLanguage@: 'Nothing'
+-- * @ignoreShebang@: 'True'
+-- * @renderCode@: 'True'
+-- * @numberCodeLines@: 'True'
+--
+-- @since 0.0.1.0
+defaultOptions :: Options
+defaultOptions = Options
+    { targetFormat    = TargetFormat.PandocMarkdown
+    , codeLanguage    = Nothing
+    , ignoreShebang   = True
+    , renderCode      = True
+    , numberCodeLines = True
+    }
+
+-- | Default options for the specified code language
+--
+-- @since 0.0.1.0
+defaultOptionsFor :: CodeLanguage -> Options
+defaultOptionsFor lang = defaultOptions
+    { codeLanguage = Just lang
+    }
+
+-- | Create a "Conduit" transformer that renders using the specified options
+--
+-- The transformer consumes 'SourceLine' values annotated with the line number
+-- and produces lines of output.
+--
+-- @since 0.0.1.0
+render
+  :: forall m. Monad m
+  => Options
+  -> ConduitT (Int, SourceLine) Text m ()
+render Options{..} = C.await >>= sStart
+  where
+    -- Start state
+    sStart
+      :: Maybe (Int, SourceLine)
+      -> ConduitT (Int, SourceLine) Text m ()
+    sStart (Just (lineNum, sourceLine)) = case sourceLine of
+      -- render shebang if enabled, otherwise skip
+      SourceLine.Shebang line
+        | renderCode && not ignoreShebang -> do
+            C.yield $ startShebang lineNum
+            C.yield line
+            C.yield endCode
+            C.await >>= sBlank
+        | otherwise -> C.await >>= sStart
+      -- start rendering documentation
+      SourceLine.Doc line -> do
+        C.yield line
+        C.await >>= sDoc
+      -- start rendering code if enabled, otherwise skip
+      SourceLine.Code line
+        | renderCode -> do
+            C.yield $ startCode lineNum
+            C.yield line
+            C.await >>= sCode 0
+        | otherwise -> C.await >>= sStart
+      -- skip blank lines and rules
+      _BlankOrRule -> C.await >>= sStart
+    sStart Nothing = return ()
+
+    -- Code state
+    sCode
+      :: Int  -- ^ number of pending (code) blank lines
+      -> Maybe (Int, SourceLine)
+      -> ConduitT (Int, SourceLine) Text m ()
+    sCode !numBlanks (Just (_lineNum, sourceLine)) = case sourceLine of
+      -- render any pending blank lines and code
+      SourceLine.Code line -> do
+        replicateM_ numBlanks $ C.yield T.empty
+        C.yield line
+        C.await >>= sCode 0
+      -- increment number of pending blank lines
+      SourceLine.CodeBlank -> C.await >>= sCode (numBlanks + 1)
+      -- end code, add blank line, start rendering documentation
+      SourceLine.Doc line -> do
+        C.yield endCode
+        C.yield T.empty
+        C.yield line
+        C.await >>= sDoc
+      -- end code, enter blank state
+      SourceLine.DocBlank -> do
+        C.yield endCode
+        C.await >>= sBlank
+      -- end code, enter blank state
+      SourceLine.Rule -> do
+        C.yield endCode
+        C.await >>= sBlank
+      -- shebang only possible on first line (seen in start state)
+      SourceLine.Shebang _line -> error "impossible: Shebang in sCode"
+    sCode _numBlanks Nothing = C.yield endCode
+
+    -- Documentation state
+    sDoc
+      :: Maybe (Int, SourceLine)
+      -> ConduitT (Int, SourceLine) Text m ()
+    sDoc (Just (lineNum, sourceLine)) = case sourceLine of
+      -- render documentation
+      SourceLine.Doc line -> do
+        C.yield line
+        C.await >>= sDoc
+      -- start rendering code if enabled, otherwise enter blank state
+      SourceLine.Code line
+        | renderCode -> do
+            C.yield T.empty
+            C.yield $ startCode lineNum
+            C.yield line
+            C.await >>= sCode 0
+        | otherwise -> C.await >>= sBlank
+      -- shebang only possible on first line (seen in start state)
+      SourceLine.Shebang _line -> error "impossible: Shebang in sDoc"
+      -- blank lines and rules transition to blank state
+      _BlankOrRule -> C.await >>= sBlank
+    sDoc Nothing = return ()
+
+    -- Blank state
+    sBlank
+      :: Maybe (Int, SourceLine)
+      -> ConduitT (Int, SourceLine) Text m ()
+    sBlank (Just (lineNum, sourceLine)) = case sourceLine of
+      -- start rendering code if enabled, otherwise skip
+      SourceLine.Code line
+        | renderCode -> do
+            C.yield T.empty
+            C.yield $ startCode lineNum
+            C.yield line
+            C.await >>= sCode 0
+        | otherwise -> C.await >>= sBlank
+      -- render blank line, start rendering documentation
+      SourceLine.Doc line -> do
+        C.yield T.empty
+        C.yield line
+        C.await >>= sDoc
+      -- shebang only possible on first line (seen in start state)
+      SourceLine.Shebang _line  -> error "impossible: Shebang in sBlank"
+      -- skip additional blank lines and rules
+      _BlankOrRule -> C.await >>= sBlank
+    sBlank Nothing = return ()
+
+    -- start code line for code starting at given line number
+    startCode :: Int -> Text
+    startCode =
+      TargetFormat.mkBeginCode targetFormat codeLanguage numberCodeLines
+
+    -- shebang start code line does not use syntax highlighting
+    startShebang :: Int -> Text
+    startShebang =
+      TargetFormat.mkBeginCode targetFormat Nothing numberCodeLines
+
+    -- end code line
+    endCode :: Text
+    endCode = TargetFormat.mkEndCode targetFormat
+{-# ANN render ("HLint: ignore Reduce duplication" :: String) #-}
diff --git a/src/LiterateX/SourceDefaults.hs b/src/LiterateX/SourceDefaults.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/SourceDefaults.hs
@@ -0,0 +1,70 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.SourceDefaults
+-- Description : default options by source extension
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+--
+-- This module provides some default options for various sources.
+------------------------------------------------------------------------------
+
+module LiterateX.SourceDefaults
+  ( -- * API
+    defaultsFor
+  , extensionDefaults
+  ) where
+
+-- https://hackage.haskell.org/package/base
+import Data.List (find, isSuffixOf)
+
+-- (literatex)
+import LiterateX.Types (CodeLanguage, SourceFormat)
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+
+------------------------------------------------------------------------------
+-- $API
+
+-- | Get the default source format and code language for the given filename
+--
+-- @since 0.0.1.0
+defaultsFor :: FilePath -> Maybe (SourceFormat, CodeLanguage)
+defaultsFor path =
+  fmap snd . flip find extensionDefaults $ flip isSuffixOf path . fst
+
+------------------------------------------------------------------------------
+
+-- | List of default options for various filename extensions
+--
+-- @since 0.0.1.0
+extensionDefaults :: [(String, (SourceFormat, CodeLanguage))]
+extensionDefaults =
+    [ (".c",    (SourceFormat.DoubleSlash,     "c"))
+    , (".clj",  (SourceFormat.LispSemicolons,  "clojure"))
+    , (".css",  (SourceFormat.DoubleSlash,     "css"))
+    , (".elm",  (SourceFormat.DoubleDash,      "elm"))
+    , (".erl",  (SourceFormat.Percent,         "erlang"))
+    , (".ex",   (SourceFormat.Hash,            "elixir"))
+    , (".exs",  (SourceFormat.Hash,            "elixir"))
+    , (".go",   (SourceFormat.DoubleSlash,     "go"))
+    , (".hs",   (SourceFormat.DoubleDash,      "haskell"))
+    , (".idr",  (SourceFormat.DoubleDash,      "idris"))
+    , (".java", (SourceFormat.DoubleSlash,     "java"))
+    , (".js",   (SourceFormat.DoubleSlash,     "javascript"))
+    , (".kt",   (SourceFormat.DoubleSlash,     "kotlin"))
+    , (".lhs",  (SourceFormat.LiterateHaskell, "haskell"))
+    , (".lisp", (SourceFormat.LispSemicolons,  "commonlisp"))
+    , (".lua",  (SourceFormat.DoubleDash,      "lua"))
+    , (".php",  (SourceFormat.DoubleSlash,     "php"))
+    , (".pl",   (SourceFormat.Hash,            "perl"))
+    , (".py",   (SourceFormat.Hash,            "python"))
+    , (".r",    (SourceFormat.Hash,            "r"))
+    , (".rb",   (SourceFormat.Hash,            "ruby"))
+    , (".rkt",  (SourceFormat.LispSemicolons,  "scheme"))
+    , (".rs",   (SourceFormat.DoubleSlash,     "rust"))
+    , (".sc",   (SourceFormat.DoubleSlash,     "scala"))
+    , (".scm",  (SourceFormat.LispSemicolons,  "scheme"))
+    , (".sh",   (SourceFormat.Hash,            "bash"))
+    , (".sql",  (SourceFormat.DoubleDash,      "sql"))
+    , (".tex",  (SourceFormat.Percent,         "latex"))
+    , (".ts",   (SourceFormat.DoubleSlash,     "typescript"))
+    ]
diff --git a/src/LiterateX/Types.hs b/src/LiterateX/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Types.hs
@@ -0,0 +1,23 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Types
+-- Description : type re-exports for convenience
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+--
+-- The type modules are generally imported qualified, so the types are
+-- re-exported by this module for convenience.
+------------------------------------------------------------------------------
+
+module LiterateX.Types
+  ( module LiterateX.Types.CodeLanguage
+  , module LiterateX.Types.SourceFormat
+  , module LiterateX.Types.SourceLine
+  , module LiterateX.Types.TargetFormat
+  ) where
+
+-- (literatex)
+import LiterateX.Types.CodeLanguage (CodeLanguage)
+import LiterateX.Types.SourceFormat (SourceFormat)
+import LiterateX.Types.SourceLine (SourceLine)
+import LiterateX.Types.TargetFormat (TargetFormat)
diff --git a/src/LiterateX/Types/CodeLanguage.hs b/src/LiterateX/Types/CodeLanguage.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Types/CodeLanguage.hs
@@ -0,0 +1,60 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Types.CodeLanguage
+-- Description : source code language type
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+------------------------------------------------------------------------------
+
+module LiterateX.Types.CodeLanguage
+  ( -- * Type
+    CodeLanguage
+  ) where
+
+-- https://hackage.haskell.org/package/base
+import Control.Monad (when)
+import Data.Bifunctor (first)
+import Data.Char (isSpace)
+import Data.String (IsString(fromString))
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import Data.Text (Text)
+
+-- https://hackage.haskell.org/package/ttc
+import qualified Data.TTC as TTC
+
+------------------------------------------------------------------------------
+-- $Type
+
+-- | Source code language
+--
+-- This string is used for syntax highlighting of source code.
+--
+-- When using Pandoc Markdown, run the following command to see a list of
+-- supported languages:
+--
+-- @
+-- \$ pandoc --list-highlight-languages
+-- @
+--
+-- When using GitHub Flavored Markdown, check the following file for supported
+-- languages:
+--
+-- <https://github.com/github/linguist/blob/master/lib/linguist/languages.yml>
+--
+-- @since 0.0.1.0
+newtype CodeLanguage = CodeLanguage { unCodeLanguage :: Text }
+  deriving (Eq, Ord, Show)
+
+instance IsString CodeLanguage where
+  fromString = either error id . TTC.parse
+
+instance TTC.Parse CodeLanguage where
+  parse = TTC.asT $ \t -> first TTC.fromS $ do
+    when (T.null t) $ Left "invalid code language: empty"
+    when (T.any isSpace t) $ Left "invalid code language: contains whitespace"
+    return $ CodeLanguage t
+
+instance TTC.Render CodeLanguage where
+  render = TTC.fromT . unCodeLanguage
diff --git a/src/LiterateX/Types/SourceFormat.hs b/src/LiterateX/Types/SourceFormat.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Types/SourceFormat.hs
@@ -0,0 +1,72 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Types.SourceFormat
+-- Description : source format type
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+------------------------------------------------------------------------------
+
+{-# LANGUAGE LambdaCase #-}
+
+module LiterateX.Types.SourceFormat
+  ( -- * Type
+    SourceFormat(..)
+    -- * API
+  , describe
+  , list
+  ) where
+
+-- https://hackage.haskell.org/package/ttc
+import qualified Data.TTC as TTC
+
+------------------------------------------------------------------------------
+-- $Type
+
+-- | Source format
+--
+-- This sum type defines the supported source formats.
+--
+-- @since 0.0.1.0
+data SourceFormat
+  = DoubleDash       -- ^ \-- comments
+  | DoubleSlash      -- ^ // comments
+  | Hash             -- ^ # comments
+  | LispSemicolons   -- ^ Lisp semicolon comments
+  | LiterateHaskell  -- ^ literate Haskell
+  | Percent          -- ^ % comments
+  deriving (Bounded, Enum, Eq, Ord, Show)
+
+instance TTC.Parse SourceFormat where
+  parse = TTC.parseEnum' "source format" True False
+
+instance TTC.Render SourceFormat where
+  render = TTC.fromS . \case
+    DoubleDash      -> "ddash"
+    DoubleSlash     -> "dslash"
+    Hash            -> "hash"
+    LispSemicolons  -> "lisp"
+    LiterateHaskell -> "lhs"
+    Percent         -> "percent"
+
+------------------------------------------------------------------------------
+-- $API
+
+-- | Get a description of a source format
+--
+-- @since 0.0.1.0
+describe :: SourceFormat -> String
+describe = \case
+    DoubleDash      -> "-- comments"
+    DoubleSlash     -> "// comments"
+    Hash            -> "# comments"
+    LispSemicolons  -> "Lisp semicolon comments"
+    LiterateHaskell -> "literate Haskell"
+    Percent         -> "% comments"
+
+------------------------------------------------------------------------------
+
+-- | List of all supported source formats
+--
+-- @since 0.0.1.0
+list :: [SourceFormat]
+list = [minBound ..]
diff --git a/src/LiterateX/Types/SourceLine.hs b/src/LiterateX/Types/SourceLine.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Types/SourceLine.hs
@@ -0,0 +1,30 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Types.SourceLine
+-- Description : source line type
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+------------------------------------------------------------------------------
+
+module LiterateX.Types.SourceLine
+  ( -- * Type
+    SourceLine(..)
+  ) where
+
+-- https://hackage.haskell.org/package/text
+import Data.Text (Text)
+
+------------------------------------------------------------------------------
+-- $Type
+
+-- | Parsed source line
+--
+-- @since 0.0.1.0
+data SourceLine
+  = Shebang !Text  -- ^ script shebang on first line
+  | CodeBlank      -- ^ code blank line
+  | DocBlank       -- ^ documentation blank line
+  | Rule           -- ^ comment rule used to organize source code
+  | Doc !Text      -- ^ documentation line
+  | Code !Text     -- ^ code line
+  deriving Show
diff --git a/src/LiterateX/Types/TargetFormat.hs b/src/LiterateX/Types/TargetFormat.hs
new file mode 100644
--- /dev/null
+++ b/src/LiterateX/Types/TargetFormat.hs
@@ -0,0 +1,115 @@
+------------------------------------------------------------------------------
+-- |
+-- Module      : LiterateX.Types.TargetFormat
+-- Description : target format type
+-- Copyright   : Copyright (c) 2021 Travis Cardwell
+-- License     : MIT
+------------------------------------------------------------------------------
+
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Types.TargetFormat
+  ( -- * Type
+    TargetFormat(..)
+    -- * API
+  , describe
+  , list
+  , mkEndCode
+  , mkBeginCode
+  ) where
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import Data.Text (Text)
+
+-- https://hackage.haskell.org/package/ttc
+import qualified Data.TTC as TTC
+
+-- (literatex)
+import LiterateX.Types.CodeLanguage (CodeLanguage)
+
+------------------------------------------------------------------------------
+-- $Type
+
+-- | Target format
+--
+-- This sum type defines the supported target formats.
+--
+-- @since 0.0.1.0
+data TargetFormat
+  = PandocMarkdown
+  | GitHubFlavoredMarkdown
+  deriving (Bounded, Enum, Eq, Ord, Show)
+
+instance TTC.Parse TargetFormat where
+  parse = TTC.parseEnum' "target format" True False
+
+instance TTC.Render TargetFormat where
+  render = TTC.fromS . \case
+    PandocMarkdown         -> "pandoc"
+    GitHubFlavoredMarkdown -> "github"
+
+------------------------------------------------------------------------------
+-- $API
+
+-- | Get a description of a target format
+--
+-- @since 0.0.1.0
+describe :: TargetFormat -> String
+describe = \case
+    PandocMarkdown         -> "Pandoc Markdown"
+    GitHubFlavoredMarkdown -> "GitHub Flavored Markdown"
+
+------------------------------------------------------------------------------
+
+-- | List of all supported target formats
+--
+-- @since 0.0.1.0
+list :: [TargetFormat]
+list = [minBound ..]
+
+------------------------------------------------------------------------------
+
+-- | Make line in the target format to end a block of source code
+--
+-- @since 0.0.1.0
+mkEndCode
+  :: TargetFormat
+  -> Text
+mkEndCode _anyFormat = "```"
+
+------------------------------------------------------------------------------
+
+-- | Make line in the target format to begin a block of code
+--
+-- Note that this function is written to indicate how it is used.  Given the
+-- target format, optional code language, and line numbering flag, this
+-- function returns a function that takes a line number and returns a line.
+--
+-- @since 0.0.1.0
+mkBeginCode
+  :: TargetFormat
+  -> Maybe CodeLanguage
+  -> Bool           -- ^ 'True' to number code lines
+  -> (Int -> Text)  -- ^ make line for code starting at specified line number
+mkBeginCode PandocMarkdown (Just lang) True = \lineNum -> T.concat
+    [ "``` {.", TTC.render lang, " .numberSource startFrom=\""
+    , TTC.renderWithShow lineNum, "\"}"
+    ]
+mkBeginCode GitHubFlavoredMarkdown (Just lang) True = \lineNum -> T.concat
+    [ "``` ", TTC.render lang, " startline=", TTC.renderWithShow lineNum
+    ]
+mkBeginCode PandocMarkdown (Just lang) False = const $ T.concat
+    [ "```", TTC.render lang
+    ]
+mkBeginCode GitHubFlavoredMarkdown (Just lang) False = const $ T.concat
+    [ "``` ", TTC.render lang
+    ]
+mkBeginCode PandocMarkdown Nothing True = \lineNum -> T.concat
+    [ "``` {.numberSource startFrom=\"", TTC.renderWithShow lineNum, "\"}"
+    ]
+mkBeginCode GitHubFlavoredMarkdown Nothing True = \lineNum -> T.concat
+    [ "``` startline=", TTC.renderWithShow lineNum
+    ]
+mkBeginCode _anyFormat Nothing False = const "```"
diff --git a/test/LiterateX/Test/API.hs b/test/LiterateX/Test/API.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/API.hs
@@ -0,0 +1,367 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.API (tests) where
+
+-- https://hackage.haskell.org/package/bytestring
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+
+-- https://hackage.haskell.org/package/filepath
+import System.FilePath ((</>))
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+
+-- https://hackage.haskell.org/package/ttc
+import qualified Data.TTC as TTC
+
+-- https://hackage.haskell.org/package/unliftio
+import qualified UnliftIO.IO as IO
+import UnliftIO.Temporary (withSystemTempDirectory)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+sourceS :: String
+sourceS = unlines
+    [ "#!/usr/bin/env python"
+    , ""
+    , "# # Example Python Program"
+    , "#"
+    , "# This is an example program."
+    , ""
+    , ""
+    , "######################################################################"
+    , ""
+    , "# ## Function `greet`"
+    , "#"
+    , "# This function prints a greeting."
+    , "def greet(name: str) -> None:"
+    , "    if name == '':"
+    , "        print('Hello!')"
+    , "        return"
+    , ""
+    , "    print(f'Hello {name}!')"
+    , ""
+    , ""
+    , "if __name__ == '__main__':"
+    , "    greet()"
+    ]
+
+sourceT :: T.Text
+sourceT = TTC.fromS sourceS
+
+sourceTL :: TL.Text
+sourceTL = TTC.fromS sourceS
+
+sourceBS :: BS.ByteString
+sourceBS = TTC.fromS sourceS
+
+sourceBSL :: BSL.ByteString
+sourceBSL = TTC.fromS sourceS
+
+------------------------------------------------------------------------------
+
+targetS :: String
+targetS = unlines
+    [ "# Example Python Program"
+    , ""
+    , "This is an example program."
+    , ""
+    , "## Function `greet`"
+    , ""
+    , "This function prints a greeting."
+    , ""
+    , "``` {.python .numberSource startFrom=\"13\"}"
+    , "def greet(name: str) -> None:"
+    , "    if name == '':"
+    , "        print('Hello!')"
+    , "        return"
+    , ""
+    , "    print(f'Hello {name}!')"
+    , ""
+    , ""
+    , "if __name__ == '__main__':"
+    , "    greet()"
+    , "```"
+    ]
+
+targetT :: T.Text
+targetT = TTC.fromS targetS
+
+targetTL :: TL.Text
+targetTL = TTC.fromS targetS
+
+targetBS :: BS.ByteString
+targetBS = TTC.fromS targetS
+
+targetBSL :: BSL.ByteString
+targetBSL = TTC.fromS targetS
+
+------------------------------------------------------------------------------
+
+withSourceHandle
+  :: String  -- ^ content
+  -> (IO.Handle -> IO a)
+  -> IO a
+withSourceHandle content action =
+    withSystemTempDirectory "literatex-test-" $ \tmpDirPath -> do
+      let sourcePath = tmpDirPath </> "source"
+      writeFile sourcePath content
+      IO.withFile sourcePath IO.ReadMode action
+
+------------------------------------------------------------------------------
+
+withTargetHandle
+  :: (IO.Handle -> IO ())
+  -> IO String
+withTargetHandle action =
+    withSystemTempDirectory "literatex-test-" $ \tmpDirPath -> do
+      let targetPath = tmpDirPath </> "target"
+      IO.withFile targetPath IO.WriteMode action
+      readFile targetPath
+
+------------------------------------------------------------------------------
+
+withSourceFile
+  :: String  -- ^ content
+  -> (FilePath -> IO a)
+  -> IO a
+withSourceFile content action =
+    withSystemTempDirectory "literatex-test-" $ \tmpDirPath -> do
+      let sourcePath = tmpDirPath </> "source"
+      writeFile sourcePath content
+      action sourcePath
+
+------------------------------------------------------------------------------
+
+withTargetFile
+  :: (FilePath -> IO ())
+  -> IO String
+withTargetFile action =
+    withSystemTempDirectory "literatex-test-" $ \tmpDirPath -> do
+      let targetPath = tmpDirPath </> "target"
+      action targetPath
+      readFile targetPath
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "python"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+------------------------------------------------------------------------------
+
+testTransformTextToText :: TestTree
+testTransformTextToText = testCase "transformTextToText" $
+    targetTL @=?
+      LiterateX.transformTextToText SourceFormat.Hash rendererOpts sourceTL
+
+------------------------------------------------------------------------------
+
+testTransformTextToHandle :: TestTree
+testTransformTextToHandle = testCase "transformTextToHandle" $ do
+    result <- withTargetHandle $
+      LiterateX.transformTextToHandle SourceFormat.Hash rendererOpts sourceTL
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testTransformTextToFile :: TestTree
+testTransformTextToFile = testCase "transformTextToFile" $ do
+    result <- withTargetFile $
+      LiterateX.transformTextToFile SourceFormat.Hash rendererOpts sourceTL
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testTransformHandleToText :: TestTree
+testTransformHandleToText = testCase "transformHandleToText" $ do
+    result <- withSourceHandle sourceS $
+      LiterateX.transformHandleToText SourceFormat.Hash rendererOpts
+    targetTL @=? result
+
+------------------------------------------------------------------------------
+
+testTransformHandleToHandle :: TestTree
+testTransformHandleToHandle = testCase "transformHandleToHandle" $ do
+    result <- withSourceHandle sourceS $ withTargetHandle .
+      LiterateX.transformHandleToHandle SourceFormat.Hash rendererOpts
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testTransformHandleToFile :: TestTree
+testTransformHandleToFile = testCase "transformHandleToFile" $ do
+    result <- withSourceHandle sourceS $ withTargetFile .
+      LiterateX.transformHandleToFile SourceFormat.Hash rendererOpts
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testTransformFileToText :: TestTree
+testTransformFileToText = testCase "transformFileToText" $ do
+    result <- withSourceFile sourceS $
+      LiterateX.transformFileToText SourceFormat.Hash rendererOpts
+    targetTL @=? result
+
+------------------------------------------------------------------------------
+
+testTransformFileToHandle :: TestTree
+testTransformFileToHandle = testCase "transformFileToHandle" $ do
+    result <- withSourceFile sourceS $ withTargetHandle .
+      LiterateX.transformFileToHandle SourceFormat.Hash rendererOpts
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testTransformFileToFile :: TestTree
+testTransformFileToFile = testCase "transformFileToFile" $ do
+    result <- withSourceFile sourceS $ withTargetFile .
+      LiterateX.transformFileToFile SourceFormat.Hash rendererOpts
+    targetS @=? result
+
+------------------------------------------------------------------------------
+
+testRunPure :: TestTree
+testRunPure = testGroup "runPure"
+    [ testCase "sourceString" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceString sourceS)
+          LiterateX.sinkLazyText
+    , testCase "sourceText" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceText sourceT)
+          LiterateX.sinkLazyText
+    , testCase "sourceLazyText" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkLazyText
+    , testCase "sourceByteString" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceByteString sourceBS)
+          LiterateX.sinkLazyText
+    , testCase "sourceLazyByteString" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyByteString sourceBSL)
+          LiterateX.sinkLazyText
+    , testCase "sinkString" $
+        targetS @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkString
+    , testCase "sinkText" $
+        targetT @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkText
+    , testCase "sinkLazyText" $
+        targetTL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkLazyText
+    , testCase "sinkByteString" $
+        targetBS @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkByteString
+    , testCase "sinkLazyByteString" $
+        targetBSL @=? LiterateX.runPure SourceFormat.Hash rendererOpts
+          (LiterateX.sourceLazyText sourceTL)
+          LiterateX.sinkLazyByteString
+    ]
+
+------------------------------------------------------------------------------
+
+testRunIO :: TestTree
+testRunIO = testGroup "runIO"
+    [ testCase "TextToHandle" $ do
+        result <- withTargetHandle $ \target ->
+          LiterateX.runIO SourceFormat.Hash rendererOpts
+            (LiterateX.sourceLazyText sourceTL)
+            (LiterateX.sinkHandle target)
+        targetS @=? result
+    , testCase "HandleToText" $ do
+        result <- withSourceHandle sourceS $ \source ->
+          LiterateX.runIO SourceFormat.Hash rendererOpts
+            (LiterateX.sourceHandle source)
+            LiterateX.sinkLazyText
+        targetTL @=? result
+    , testCase "HandleToHandle" $ do
+        result <- withSourceHandle sourceS $ \source ->
+          withTargetHandle $ \target ->
+            LiterateX.runIO SourceFormat.Hash rendererOpts
+              (LiterateX.sourceHandle source)
+              (LiterateX.sinkHandle target)
+        targetS @=? result
+    ]
+
+------------------------------------------------------------------------------
+
+testRunResource :: TestTree
+testRunResource = testGroup "runResource"
+    [ testCase "TextToFile" $ do
+        result <- withTargetFile $ \target ->
+          LiterateX.runResource SourceFormat.Hash rendererOpts
+            (LiterateX.sourceLazyText sourceTL)
+            (LiterateX.sinkFile target)
+        targetS @=? result
+    , testCase "HandleToFile" $ do
+        result <- withSourceHandle sourceS $ \source ->
+          withTargetFile $ \target ->
+            LiterateX.runResource SourceFormat.Hash rendererOpts
+              (LiterateX.sourceHandle source)
+              (LiterateX.sinkFile target)
+        targetS @=? result
+    , testCase "FileToText" $ do
+        result <- withSourceFile sourceS $ \source ->
+          LiterateX.runResource SourceFormat.Hash rendererOpts
+            (LiterateX.sourceFile source)
+            LiterateX.sinkLazyText
+        targetTL @=? result
+    , testCase "FileToHandle" $ do
+        result <- withSourceFile sourceS $ \source ->
+          withTargetHandle $ \target ->
+            LiterateX.runResource SourceFormat.Hash rendererOpts
+              (LiterateX.sourceFile source)
+              (LiterateX.sinkHandle target)
+        targetS @=? result
+    , testCase "FileToFile" $ do
+        result <- withSourceFile sourceS $ \source ->
+          withTargetFile $ \target ->
+            LiterateX.runResource SourceFormat.Hash rendererOpts
+              (LiterateX.sourceFile source)
+              (LiterateX.sinkFile target)
+        targetS @=? result
+    ]
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "LiterateX.Test.API"
+    [ testTransformTextToText
+    , testTransformTextToHandle
+    , testTransformTextToFile
+    , testTransformHandleToText
+    , testTransformHandleToHandle
+    , testTransformHandleToFile
+    , testTransformFileToText
+    , testTransformFileToHandle
+    , testTransformFileToFile
+    , testRunPure
+    , testRunIO
+    , testRunResource
+    ]
diff --git a/test/LiterateX/Test/SourceFormat.hs b/test/LiterateX/Test/SourceFormat.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat.hs
@@ -0,0 +1,24 @@
+module LiterateX.Test.SourceFormat (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- (literatex:test)
+import qualified LiterateX.Test.SourceFormat.DoubleDash
+import qualified LiterateX.Test.SourceFormat.DoubleSlash
+import qualified LiterateX.Test.SourceFormat.Hash
+import qualified LiterateX.Test.SourceFormat.LispSemicolons
+import qualified LiterateX.Test.SourceFormat.LiterateHaskell
+import qualified LiterateX.Test.SourceFormat.Percent
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "LiterateX.Test.SourceFormat"
+    [ LiterateX.Test.SourceFormat.DoubleDash.tests
+    , LiterateX.Test.SourceFormat.DoubleSlash.tests
+    , LiterateX.Test.SourceFormat.Hash.tests
+    , LiterateX.Test.SourceFormat.LispSemicolons.tests
+    , LiterateX.Test.SourceFormat.LiterateHaskell.tests
+    , LiterateX.Test.SourceFormat.Percent.tests
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/DoubleDash.hs b/test/LiterateX/Test/SourceFormat/DoubleDash.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/DoubleDash.hs
@@ -0,0 +1,871 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.DoubleDash (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "haskell"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.DoubleDash
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env stack"
+    , ""
+    , "-- This results in warnings!"
+    , ""
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "This results in warnings!"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"5\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env stack"
+    , "```"
+    , ""
+    , "This results in warnings!"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"5\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "This results in warnings!"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env stack"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , "-- Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env stack"
+    , "```"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "-- Test"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"3\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , "-- Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ "--"
+    , "-- Test"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , "-- Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceStartRuleDoc :: Text
+sourceStartRuleDoc = TL.unlines
+    [ "----------------------------------------------------------------------"
+    , "-- Test"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetStartRuleDoc :: Text
+targetStartRuleDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testStartRuleDoc :: TestTree
+testStartRuleDoc = testCase "startRuleDoc" $
+    targetStartRuleDoc @=? run sourceStartRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceStartRuleCode :: Text
+sourceStartRuleCode = TL.unlines
+    [ "----------------------------------------------------------------------"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , "-- Test"
+    ]
+
+targetStartRuleCode :: Text
+targetStartRuleCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"3\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartRuleCode :: TestTree
+testStartRuleCode = testCase "startRuleCode" $
+    targetStartRuleCode @=? run sourceStartRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "-- Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , ""
+    , "-- Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "--"
+    , "--"
+    , "-- Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "module Main (main) where"
+    , "--"
+    , "--"
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , "```"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleCode :: Text
+sourceCodeRuleCode = TL.unlines
+    [ "module Main (main) where"
+    , "----------------------------------------------------------------------"
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetCodeRuleCode :: Text
+targetCodeRuleCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , "```"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"3\"}"
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testCodeRuleCode :: TestTree
+testCodeRuleCode = testCase "codeRuleCode" $
+    targetCodeRuleCode @=? run sourceCodeRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleDoc :: Text
+sourceCodeRuleDoc = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "----------------------------------------------------------------------"
+    , "-- Test"
+    ]
+
+targetCodeRuleDoc :: Text
+targetCodeRuleDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeRuleDoc :: TestTree
+testCodeRuleDoc = testCase "codeRuleDoc" $
+    targetCodeRuleDoc @=? run sourceCodeRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "-- Test"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "-- one"
+    , "--"
+    , "--"
+    , "-- two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "-- Test"
+    , "--"
+    , "--"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "-- Test"
+    , ""
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "-- one"
+    , ""
+    , ""
+    , "-- two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleDoc :: Text
+sourceDocRuleDoc = TL.unlines
+    [ "-- one"
+    , "----------------------------------------------------------------------"
+    , "-- two"
+    ]
+
+targetDocRuleDoc :: Text
+targetDocRuleDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocRuleDoc :: TestTree
+testDocRuleDoc = testCase "docRuleDoc" $
+    targetDocRuleDoc @=? run sourceDocRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleCode :: Text
+sourceDocRuleCode = TL.unlines
+    [ "-- Test"
+    , "----------------------------------------------------------------------"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetDocRuleCode :: Text
+targetDocRuleCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"3\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocRuleCode :: TestTree
+testDocRuleCode = testCase "docRuleCode" $
+    targetDocRuleCode @=? run sourceDocRuleCode
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "-- Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ "--"
+    , "--"
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , ""
+    , ""
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyRule :: Text
+sourceOnlyRule = "-----------------------------------------------------------"
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ "--"
+    , "--"
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyRule :: TestTree
+testOnlyRule = testCase "onlyRule" $
+    targetEmpty @=? run sourceOnlyRule
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "DoubleDash"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testStartRuleDoc
+    , testStartRuleCode
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testCodeRuleCode
+    , testCodeRuleDoc
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testDocRuleDoc
+    , testDocRuleCode
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyRule
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/DoubleSlash.hs b/test/LiterateX/Test/SourceFormat/DoubleSlash.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/DoubleSlash.hs
@@ -0,0 +1,827 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.DoubleSlash (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "rust"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.DoubleSlash
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env run-cargo-script"
+    , ""
+    , "// Test"
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"5\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env run-cargo-script"
+    , "```"
+    , ""
+    , "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"5\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env run-cargo-script"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , "// Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"2\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env run-cargo-script"
+    , "```"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"2\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "// Test"
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"3\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , "// Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ "//"
+    , "// Test"
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"4\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , "// Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"2\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceStartRuleDoc :: Text
+sourceStartRuleDoc = TL.unlines
+    [ "//////////////////////////////////////////////////////////////////////"
+    , "// Test"
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetStartRuleDoc :: Text
+targetStartRuleDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"4\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testStartRuleDoc :: TestTree
+testStartRuleDoc = testCase "startRuleDoc" $
+    targetStartRuleDoc @=? run sourceStartRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceStartRuleCode :: Text
+sourceStartRuleCode = TL.unlines
+    [ "//////////////////////////////////////////////////////////////////////"
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , "// Test"
+    ]
+
+targetStartRuleCode :: Text
+targetStartRuleCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"3\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartRuleCode :: TestTree
+testStartRuleCode = testCase "startRuleCode" $
+    targetStartRuleCode @=? run sourceStartRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "// Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello...\");"
+    , ""
+    , ""
+    , "    println!(\"world!\");"
+    , "}"
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello...\");"
+    , ""
+    , ""
+    , "    println!(\"world!\");"
+    , "}"
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , ""
+    , "// Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "//"
+    , "//"
+    , "// Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello...\");"
+    , "//"
+    , "//"
+    , "    println!(\"world!\");"
+    , "}"
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello...\");"
+    , "```"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"5\"}"
+    , "    println!(\"world!\");"
+    , "}"
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleCode :: Text
+sourceCodeRuleCode = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello...\");"
+    , "//////////////////////////////////////////////////////////////////////"
+    , "    println!(\"world!\");"
+    , "}"
+    ]
+
+targetCodeRuleCode :: Text
+targetCodeRuleCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello...\");"
+    , "```"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"4\"}"
+    , "    println!(\"world!\");"
+    , "}"
+    , "```"
+    ]
+
+testCodeRuleCode :: TestTree
+testCodeRuleCode = testCase "codeRuleCode" $
+    targetCodeRuleCode @=? run sourceCodeRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleDoc :: Text
+sourceCodeRuleDoc = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "//////////////////////////////////////////////////////////////////////"
+    , "// Test"
+    ]
+
+targetCodeRuleDoc :: Text
+targetCodeRuleDoc = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeRuleDoc :: TestTree
+testCodeRuleDoc = testCase "codeRuleDoc" $
+    targetCodeRuleDoc @=? run sourceCodeRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "// Test"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"2\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "// one"
+    , "//"
+    , "//"
+    , "// two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "// Test"
+    , "//"
+    , "//"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"4\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "// Test"
+    , ""
+    , ""
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"4\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "// one"
+    , ""
+    , ""
+    , "// two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleDoc :: Text
+sourceDocRuleDoc = TL.unlines
+    [ "// one"
+    , "//////////////////////////////////////////////////////////////////////"
+    , "// two"
+    ]
+
+targetDocRuleDoc :: Text
+targetDocRuleDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocRuleDoc :: TestTree
+testDocRuleDoc = testCase "docRuleDoc" $
+    targetDocRuleDoc @=? run sourceDocRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleCode :: Text
+sourceDocRuleCode = TL.unlines
+    [ "// Test"
+    , "//////////////////////////////////////////////////////////////////////"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetDocRuleCode :: Text
+targetDocRuleCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.rust .numberSource startFrom=\"3\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testDocRuleCode :: TestTree
+testDocRuleCode = testCase "docRuleCode" $
+    targetDocRuleCode @=? run sourceDocRuleCode
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "// Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ "//"
+    , "//"
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , ""
+    , ""
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.rust .numberSource startFrom=\"1\"}"
+    , "fn main() {"
+    , "    println!(\"Hello!\");"
+    , "}"
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyRule :: Text
+sourceOnlyRule = "///////////////////////////////////////////////////////////"
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ "//"
+    , "//"
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyRule :: TestTree
+testOnlyRule = testCase "onlyRule" $
+    targetEmpty @=? run sourceOnlyRule
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "DoubleSlash"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testStartRuleDoc
+    , testStartRuleCode
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testCodeRuleCode
+    , testCodeRuleDoc
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testDocRuleDoc
+    , testDocRuleCode
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyRule
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/Hash.hs b/test/LiterateX/Test/SourceFormat/Hash.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/Hash.hs
@@ -0,0 +1,740 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.Hash (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "python"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.Hash
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env python"
+    , ""
+    , "# Test"
+    , ""
+    , "print('Hello!')"
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"5\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env python"
+    , "```"
+    , ""
+    , "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"5\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env python"
+    , ""
+    , "print('Hello!')"
+    , ""
+    , "# Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('Hello!')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env python"
+    , "```"
+    , ""
+    , "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('Hello!')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "# Test"
+    , ""
+    , "print('Hello!')"
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "print('Hello!')"
+    , ""
+    , "# Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('Hello!')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ "#"
+    , "# Test"
+    , ""
+    , "print('Hello!')"
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"4\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ""
+    , "print('Hello!')"
+    , ""
+    , "# Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"2\"}"
+    , "print('Hello!')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceStartRuleDoc :: Text
+sourceStartRuleDoc = TL.unlines
+    [ "######################################################################"
+    , "# Test"
+    , ""
+    , "print('Hello!')"
+    ]
+
+targetStartRuleDoc :: Text
+targetStartRuleDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"4\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testStartRuleDoc :: TestTree
+testStartRuleDoc = testCase "startRuleDoc" $
+    targetStartRuleDoc @=? run sourceStartRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceStartRuleCode :: Text
+sourceStartRuleCode = TL.unlines
+    [ "######################################################################"
+    , ""
+    , "print('Hello!')"
+    , ""
+    , "# Test"
+    ]
+
+targetStartRuleCode :: Text
+targetStartRuleCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('Hello!')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartRuleCode :: TestTree
+testStartRuleCode = testCase "startRuleCode" $
+    targetStartRuleCode @=? run sourceStartRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "print('one')"
+    , "# Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "print('one')"
+    , ""
+    , ""
+    , "print('two')"
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , ""
+    , ""
+    , "print('two')"
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "print('one')"
+    , ""
+    , ""
+    , "# Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "print('one')"
+    , "#"
+    , "#"
+    , "# Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "print('one')"
+    , "#"
+    , "#"
+    , "print('two')"
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "``` {.python .numberSource startFrom=\"4\"}"
+    , "print('two')"
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleCode :: Text
+sourceCodeRuleCode = TL.unlines
+    [ "print('one')"
+    , "######################################################################"
+    , "print('two')"
+    ]
+
+targetCodeRuleCode :: Text
+targetCodeRuleCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('two')"
+    , "```"
+    ]
+
+testCodeRuleCode :: TestTree
+testCodeRuleCode = testCase "codeRuleCode" $
+    targetCodeRuleCode @=? run sourceCodeRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleDoc :: Text
+sourceCodeRuleDoc = TL.unlines
+    [ "print('one')"
+    , "######################################################################"
+    , "# Test"
+    ]
+
+targetCodeRuleDoc :: Text
+targetCodeRuleDoc = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeRuleDoc :: TestTree
+testCodeRuleDoc = testCase "codeRuleDoc" $
+    targetCodeRuleDoc @=? run sourceCodeRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "# Test"
+    , "print('one')"
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"2\"}"
+    , "print('one')"
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "# one"
+    , "#"
+    , "#"
+    , "# two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "# Test"
+    , "#"
+    , "#"
+    , "print('Hello!')"
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"4\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "# Test"
+    , ""
+    , ""
+    , "print('Hello!')"
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"4\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "# one"
+    , ""
+    , ""
+    , "# two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleDoc :: Text
+sourceDocRuleDoc = TL.unlines
+    [ "# one"
+    , "######################################################################"
+    , "# two"
+    ]
+
+targetDocRuleDoc :: Text
+targetDocRuleDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocRuleDoc :: TestTree
+testDocRuleDoc = testCase "docRuleDoc" $
+    targetDocRuleDoc @=? run sourceDocRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleCode :: Text
+sourceDocRuleCode = TL.unlines
+    [ "# Test"
+    , "######################################################################"
+    , "print('Hello!')"
+    ]
+
+targetDocRuleCode :: Text
+targetDocRuleCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"3\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testDocRuleCode :: TestTree
+testDocRuleCode = testCase "docRuleCode" $
+    targetDocRuleCode @=? run sourceDocRuleCode
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "# Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ "#"
+    , "#"
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "print('Hello!')"
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "print('Hello!')"
+    , ""
+    , ""
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.python .numberSource startFrom=\"1\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyRule :: Text
+sourceOnlyRule = "###########################################################"
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ "#"
+    , "#"
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyRule :: TestTree
+testOnlyRule = testCase "onlyRule" $
+    targetEmpty @=? run sourceOnlyRule
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "Hash"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testStartRuleDoc
+    , testStartRuleCode
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testCodeRuleCode
+    , testCodeRuleDoc
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testDocRuleDoc
+    , testDocRuleCode
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyRule
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/LispSemicolons.hs b/test/LiterateX/Test/SourceFormat/LispSemicolons.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/LispSemicolons.hs
@@ -0,0 +1,837 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.LispSemicolons (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "scheme"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.LispSemicolons
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env racket"
+    , ""
+    , "; Test"
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"5\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env racket"
+    , "```"
+    , ""
+    , "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"5\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env racket"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , ""
+    , "; Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"2\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env racket"
+    , "```"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"2\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "; Test"
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"3\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , ""
+    , "; Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ ";"
+    , "; Test"
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"4\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , ""
+    , "; Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"2\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceStartRuleDoc :: Text
+sourceStartRuleDoc = TL.unlines
+    [ ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , "; Test"
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetStartRuleDoc :: Text
+targetStartRuleDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"4\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testStartRuleDoc :: TestTree
+testStartRuleDoc = testCase "startRuleDoc" $
+    targetStartRuleDoc @=? run sourceStartRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceStartRuleCode :: Text
+sourceStartRuleCode = TL.unlines
+    [ ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , ""
+    , "; Test"
+    ]
+
+targetStartRuleCode :: Text
+targetStartRuleCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"3\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartRuleCode :: TestTree
+testStartRuleCode = testCase "startRuleCode" $
+    targetStartRuleCode @=? run sourceStartRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "; Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , ""
+    , "print('one')"
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , ""
+    , "print('one')"
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "print('one')"
+    , ""
+    , ""
+    , "; Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "print('one')"
+    , ";"
+    , ";"
+    , "; Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "#lang racket/base"
+    , ";"
+    , ";"
+    , "print('one')"
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , "```"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"4\"}"
+    , "print('one')"
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleCode :: Text
+sourceCodeRuleCode = TL.unlines
+    [ "#lang racket/base"
+    , ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , "print('one')"
+    ]
+
+targetCodeRuleCode :: Text
+targetCodeRuleCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , "```"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"3\"}"
+    , "print('one')"
+    , "```"
+    ]
+
+testCodeRuleCode :: TestTree
+testCodeRuleCode = testCase "codeRuleCode" $
+    targetCodeRuleCode @=? run sourceCodeRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleDoc :: Text
+sourceCodeRuleDoc = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "print('one')"
+    , ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , "; Test"
+    ]
+
+targetCodeRuleDoc :: Text
+targetCodeRuleDoc = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeRuleDoc :: TestTree
+testCodeRuleDoc = testCase "codeRuleDoc" $
+    targetCodeRuleDoc @=? run sourceCodeRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "; Test"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"2\"}"
+    , "#lang racket/base"
+    , ""
+    , "print('one')"
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "; one"
+    , ";"
+    , ";"
+    , "; two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "; Test"
+    , ";"
+    , ";"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"4\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "; Test"
+    , ""
+    , ""
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"4\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "; one"
+    , ""
+    , ""
+    , "; two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleDoc :: Text
+sourceDocRuleDoc = TL.unlines
+    [ "; one"
+    , ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , "; two"
+    ]
+
+targetDocRuleDoc :: Text
+targetDocRuleDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocRuleDoc :: TestTree
+testDocRuleDoc = testCase "docRuleDoc" $
+    targetDocRuleDoc @=? run sourceDocRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleCode :: Text
+sourceDocRuleCode = TL.unlines
+    [ "; Test"
+    , ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetDocRuleCode :: Text
+targetDocRuleCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.scheme .numberSource startFrom=\"3\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testDocRuleCode :: TestTree
+testDocRuleCode = testCase "docRuleCode" $
+    targetDocRuleCode @=? run sourceDocRuleCode
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "; Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ ";"
+    , ";"
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , ""
+    , ""
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.scheme .numberSource startFrom=\"1\"}"
+    , "#lang racket/base"
+    , ""
+    , "(println \"Hello!\")"
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyRule :: Text
+sourceOnlyRule = ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ ";"
+    , ";"
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyRule :: TestTree
+testOnlyRule = testCase "onlyRule" $
+    targetEmpty @=? run sourceOnlyRule
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+testLispSemicolons :: TestTree
+testLispSemicolons = testGroup "lispSemicolons"
+    [ testCase "oneSemicolonDoc" $
+        target @=? run (sourceSemicolonDoc 1)
+    , testCase "twoSemicolonDoc" $
+        target @=? run (sourceSemicolonDoc 2)
+    , testCase "threeSemicolonDoc" $
+        target @=? run (sourceSemicolonDoc 3)
+    , testCase "fourSemicolonDoc" $
+        target @=? run (sourceSemicolonDoc 4)
+    ]
+  where
+    sourceSemicolonDoc :: Int -> Text
+    sourceSemicolonDoc count =
+      TL.concat [TL.pack (replicate count ';'), " ", target]
+
+    target :: Text
+    target = TL.unlines ["Test"]
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "LispSemicolons"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testStartRuleDoc
+    , testStartRuleCode
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testCodeRuleCode
+    , testCodeRuleDoc
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testDocRuleDoc
+    , testDocRuleCode
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyRule
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    , testLispSemicolons
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/LiterateHaskell.hs b/test/LiterateX/Test/SourceFormat/LiterateHaskell.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/LiterateHaskell.hs
@@ -0,0 +1,696 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.LiterateHaskell (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "haskell"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.LiterateHaskell
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env stack"
+    , ""
+    , "This results in warnings!"
+    , ""
+    , "> {- stack script --resolver lts-17.11 -}"
+    , ">"
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "This results in warnings!"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"5\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env stack"
+    , "```"
+    , ""
+    , "This results in warnings!"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"5\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "This results in warnings!"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env stack"
+    , "> {- stack script --resolver lts-17.11 -}"
+    , ">"
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env stack"
+    , "```"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "{- stack script --resolver lts-17.11 -}"
+    , ""
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"3\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ""
+    , "Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ ""
+    , "Test"
+    , ""
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ">"
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ""
+    , "Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , "Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ">"
+    , ">"
+    , "Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ""
+    , ""
+    , "Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "> module Main (main) where"
+    , ""
+    , ""
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , "```"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "Test"
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"2\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , ""
+    , "two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , ""
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ">"
+    , ">"
+    , "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.haskell .numberSource startFrom=\"4\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ">"
+    , ">"
+    , "two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "> module Main (main) where"
+    , ">"
+    , "> main :: IO ()"
+    , "> main = putStrLn \"Hello!\""
+    , ">"
+    , ">"
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.haskell .numberSource startFrom=\"1\"}"
+    , "module Main (main) where"
+    , ""
+    , "main :: IO ()"
+    , "main = putStrLn \"Hello!\""
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ">"
+    , ">"
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "LiterateHaskell"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    ]
diff --git a/test/LiterateX/Test/SourceFormat/Percent.hs b/test/LiterateX/Test/SourceFormat/Percent.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/SourceFormat/Percent.hs
@@ -0,0 +1,860 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.SourceFormat.Percent (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "erlang"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+run' :: Text -> Renderer.Options -> Text
+run' = flip $ LiterateX.transformTextToText SourceFormat.Percent
+
+run :: Text -> Text
+run source = run' source rendererOpts
+
+------------------------------------------------------------------------------
+
+sourceStartShebangDoc :: Text
+sourceStartShebangDoc = TL.unlines
+    [ "#!/usr/bin/env escript"
+    , ""
+    , "% Test"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetStartShebangDocIgnore :: Text
+targetStartShebangDocIgnore = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"5\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+targetStartShebangDocNoIgnore :: Text
+targetStartShebangDocNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env escript"
+    , "```"
+    , ""
+    , "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"5\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+targetStartShebangDocNoCode :: Text
+targetStartShebangDocNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangDoc :: TestTree
+testStartShebangDoc = testGroup "startShebangDoc"
+    [ testCase "Ignore" $
+        targetStartShebangDocIgnore @=? run sourceStartShebangDoc
+    , testCase "NoIgnore" $
+        targetStartShebangDocNoIgnore @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangDocNoCode @=?
+          run' sourceStartShebangDoc rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartShebangCode :: Text
+sourceStartShebangCode = TL.unlines
+    [ "#!/usr/bin/env escript"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , "% Test"
+    ]
+
+targetStartShebangCodeIgnore :: Text
+targetStartShebangCodeIgnore = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"3\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoIgnore :: Text
+targetStartShebangCodeNoIgnore = TL.unlines
+    [ "``` {.numberSource startFrom=\"1\"}"
+    , "#!/usr/bin/env escript"
+    , "```"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"3\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+targetStartShebangCodeNoCode :: Text
+targetStartShebangCodeNoCode = TL.unlines
+    [ "Test"
+    ]
+
+testStartShebangCode :: TestTree
+testStartShebangCode = testGroup "startShebangCode"
+    [ testCase "Ignore" $
+        targetStartShebangCodeIgnore @=? run sourceStartShebangCode
+    , testCase "NoIgnore" $
+        targetStartShebangCodeNoIgnore @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            }
+    , testCase "NoCode" $
+        targetStartShebangCodeNoCode @=?
+          run' sourceStartShebangCode rendererOpts
+            { Renderer.ignoreShebang = False
+            , Renderer.renderCode    = False
+            }
+    ]
+
+------------------------------------------------------------------------------
+
+sourceStartDoc :: Text
+sourceStartDoc = TL.unlines
+    [ "% Test"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetStartDoc :: Text
+targetStartDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"3\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testStartDoc :: TestTree
+testStartDoc = testCase "startDoc" $
+    targetStartDoc @=? run sourceStartDoc
+
+------------------------------------------------------------------------------
+
+sourceStartCode :: Text
+sourceStartCode = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , "% Test"
+    ]
+
+targetStartCode :: Text
+targetStartCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCode :: TestTree
+testStartCode = testCase "startCode" $
+    targetStartCode @=? run sourceStartCode
+
+------------------------------------------------------------------------------
+
+sourceStartDocBlank :: Text
+sourceStartDocBlank = TL.unlines
+    [ "%"
+    , "% Test"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetStartDocBlank :: Text
+targetStartDocBlank = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"4\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testStartDocBlank :: TestTree
+testStartDocBlank = testCase "startDocBlank" $
+    targetStartDocBlank @=? run sourceStartDocBlank
+
+------------------------------------------------------------------------------
+
+sourceStartCodeBlank :: Text
+sourceStartCodeBlank = TL.unlines
+    [ ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , "% Test"
+    ]
+
+targetStartCodeBlank :: Text
+targetStartCodeBlank = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"2\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartCodeBlank :: TestTree
+testStartCodeBlank = testCase "startCodeBlank" $
+    targetStartCodeBlank @=? run sourceStartCodeBlank
+
+------------------------------------------------------------------------------
+
+sourceStartRuleDoc :: Text
+sourceStartRuleDoc = TL.unlines
+    [ "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , "% Test"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetStartRuleDoc :: Text
+targetStartRuleDoc = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"4\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testStartRuleDoc :: TestTree
+testStartRuleDoc = testCase "startRuleDoc" $
+    targetStartRuleDoc @=? run sourceStartRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceStartRuleCode :: Text
+sourceStartRuleCode = TL.unlines
+    [ "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , "% Test"
+    ]
+
+targetStartRuleCode :: Text
+targetStartRuleCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"3\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testStartRuleCode :: TestTree
+testStartRuleCode = testCase "startRuleCode" $
+    targetStartRuleCode @=? run sourceStartRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeDoc :: Text
+sourceCodeDoc = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "% Test"
+    ]
+
+targetCodeDoc :: Text
+targetCodeDoc = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDoc :: TestTree
+testCodeDoc = testCase "codeDoc" $
+    targetCodeDoc @=? run sourceCodeDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksCode :: Text
+sourceCodeCodeBlanksCode = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetCodeCodeBlanksCode :: Text
+targetCodeCodeBlanksCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testCodeCodeBlanksCode :: TestTree
+testCodeCodeBlanksCode = testCase "codeCodeBlanksCode" $
+    targetCodeCodeBlanksCode @=? run sourceCodeCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeCodeBlanksDoc :: Text
+sourceCodeCodeBlanksDoc = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , ""
+    , "% Test"
+    ]
+
+targetCodeCodeBlanksDoc :: Text
+targetCodeCodeBlanksDoc = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeCodeBlanksDoc :: TestTree
+testCodeCodeBlanksDoc = testCase "codeCodeBlanksDoc" $
+    targetCodeCodeBlanksDoc @=? run sourceCodeCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksDoc :: Text
+sourceCodeDocBlanksDoc = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "%"
+    , "%"
+    , "% Test"
+    ]
+
+targetCodeDocBlanksDoc :: Text
+targetCodeDocBlanksDoc = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeDocBlanksDoc :: TestTree
+testCodeDocBlanksDoc = testCase "codeDocBlanksDoc" $
+    targetCodeDocBlanksDoc @=? run sourceCodeDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceCodeDocBlanksCode :: Text
+sourceCodeDocBlanksCode = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , "%"
+    , "%"
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetCodeDocBlanksCode :: Text
+targetCodeDocBlanksCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , "```"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"5\"}"
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testCodeDocBlanksCode :: TestTree
+testCodeDocBlanksCode = testCase "codeDocBlanksCode" $
+    targetCodeDocBlanksCode @=? run sourceCodeDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleCode :: Text
+sourceCodeRuleCode = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetCodeRuleCode :: Text
+targetCodeRuleCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , "```"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"4\"}"
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testCodeRuleCode :: TestTree
+testCodeRuleCode = testCase "codeRuleCode" $
+    targetCodeRuleCode @=? run sourceCodeRuleCode
+
+------------------------------------------------------------------------------
+
+sourceCodeRuleDoc :: Text
+sourceCodeRuleDoc = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , "% Test"
+    ]
+
+targetCodeRuleDoc :: Text
+targetCodeRuleDoc = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    , ""
+    , "Test"
+    ]
+
+testCodeRuleDoc :: TestTree
+testCodeRuleDoc = testCase "codeRuleDoc" $
+    targetCodeRuleDoc @=? run sourceCodeRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocCode :: Text
+sourceDocCode = TL.unlines
+    [ "% Test"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetDocCode :: Text
+targetDocCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"2\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testDocCode :: TestTree
+testDocCode = testCase "docCode" $
+    targetDocCode @=? run sourceDocCode
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksDoc :: Text
+sourceDocDocBlanksDoc = TL.unlines
+    [ "% one"
+    , "%"
+    , "%"
+    , "% two"
+    ]
+
+targetDocDocBlanksDoc :: Text
+targetDocDocBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocDocBlanksDoc :: TestTree
+testDocDocBlanksDoc = testCase "docDocBlanksDoc" $
+    targetDocDocBlanksDoc @=? run sourceDocDocBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocDocBlanksCode :: Text
+sourceDocDocBlanksCode = TL.unlines
+    [ "% Test"
+    , "%"
+    , "%"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetDocDocBlanksCode :: Text
+targetDocDocBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"4\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testDocDocBlanksCode :: TestTree
+testDocDocBlanksCode = testCase "docDocBlanksCode" $
+    targetDocDocBlanksCode @=? run sourceDocDocBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksCode :: Text
+sourceDocCodeBlanksCode = TL.unlines
+    [ "% Test"
+    , ""
+    , ""
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetDocCodeBlanksCode :: Text
+targetDocCodeBlanksCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"4\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testDocCodeBlanksCode :: TestTree
+testDocCodeBlanksCode = testCase "docCodeBlanksCode" $
+    targetDocCodeBlanksCode @=? run sourceDocCodeBlanksCode
+
+------------------------------------------------------------------------------
+
+sourceDocCodeBlanksDoc :: Text
+sourceDocCodeBlanksDoc = TL.unlines
+    [ "% one"
+    , ""
+    , ""
+    , "% two"
+    ]
+
+targetDocCodeBlanksDoc :: Text
+targetDocCodeBlanksDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocCodeBlanksDoc :: TestTree
+testDocCodeBlanksDoc = testCase "docCodeBlanksDoc" $
+    targetDocCodeBlanksDoc @=? run sourceDocCodeBlanksDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleDoc :: Text
+sourceDocRuleDoc = TL.unlines
+    [ "% one"
+    , "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , "% two"
+    ]
+
+targetDocRuleDoc :: Text
+targetDocRuleDoc = TL.unlines
+    [ "one"
+    , ""
+    , "two"
+    ]
+
+testDocRuleDoc :: TestTree
+testDocRuleDoc = testCase "docRuleDoc" $
+    targetDocRuleDoc @=? run sourceDocRuleDoc
+
+------------------------------------------------------------------------------
+
+sourceDocRuleCode :: Text
+sourceDocRuleCode = TL.unlines
+    [ "% Test"
+    , "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetDocRuleCode :: Text
+targetDocRuleCode = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.erlang .numberSource startFrom=\"3\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testDocRuleCode :: TestTree
+testDocRuleCode = testCase "docRuleCode" $
+    targetDocRuleCode @=? run sourceDocRuleCode
+
+------------------------------------------------------------------------------
+
+sourceEndDoc :: Text
+sourceEndDoc = TL.unlines
+    [ "% Test"
+    ]
+
+targetEndDoc :: Text
+targetEndDoc = TL.unlines
+    [ "Test"
+    ]
+
+testEndDoc :: TestTree
+testEndDoc = testCase "endDoc" $
+    targetEndDoc @=? run sourceEndDoc
+
+------------------------------------------------------------------------------
+
+sourceEndDocBlanks :: Text
+sourceEndDocBlanks = TL.unlines
+    [ "%"
+    , "%"
+    ]
+
+targetEndDocBlanks :: Text
+targetEndDocBlanks = ""
+
+testEndDocBlanks :: TestTree
+testEndDocBlanks = testCase "endDocBlanks" $
+    targetEndDocBlanks @=? run sourceEndDocBlanks
+
+------------------------------------------------------------------------------
+
+sourceEndCode :: Text
+sourceEndCode = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    ]
+
+targetEndCode :: Text
+targetEndCode = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testEndCode :: TestTree
+testEndCode = testCase "endCode" $
+    targetEndCode @=? run sourceEndCode
+
+------------------------------------------------------------------------------
+
+sourceEndCodeBlanks :: Text
+sourceEndCodeBlanks = TL.unlines
+    [ "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , ""
+    , ""
+    ]
+
+targetEndCodeBlanks :: Text
+targetEndCodeBlanks = TL.unlines
+    [ "``` {.erlang .numberSource startFrom=\"1\"}"
+    , "-module(main)."
+    , "-export([start/0])."
+    , ""
+    , "start() -> io:fwrite(\"Hello!\\n\")."
+    , "```"
+    ]
+
+testEndCodeBlanks :: TestTree
+testEndCodeBlanks = testCase "endCodeBlanks" $
+    targetEndCodeBlanks @=? run sourceEndCodeBlanks
+
+------------------------------------------------------------------------------
+
+sourceEmpty :: Text
+sourceEmpty = ""
+
+sourceOnlyRule :: Text
+sourceOnlyRule = "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
+
+sourceOnlyDocBlanks :: Text
+sourceOnlyDocBlanks = TL.unlines
+    [ "%"
+    , "%"
+    ]
+
+sourceOnlyCodeBlanks :: Text
+sourceOnlyCodeBlanks = TL.unlines
+    [ ""
+    , ""
+    ]
+
+targetEmpty :: Text
+targetEmpty = ""
+
+testEmpty :: TestTree
+testEmpty = testCase "empty" $
+    targetEmpty @=? run sourceEmpty
+
+testOnlyRule :: TestTree
+testOnlyRule = testCase "onlyRule" $
+    targetEmpty @=? run sourceOnlyRule
+
+testOnlyDocBlanks :: TestTree
+testOnlyDocBlanks = testCase "onlyDocBlanks" $
+    targetEmpty @=? run sourceOnlyDocBlanks
+
+testOnlyCodeBlanks :: TestTree
+testOnlyCodeBlanks = testCase "onlyCodeBlanks" $
+    targetEmpty @=? run sourceOnlyCodeBlanks
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "Percent"
+    [ testStartShebangDoc
+    , testStartShebangCode
+    , testStartDoc
+    , testStartCode
+    , testStartDocBlank
+    , testStartCodeBlank
+    , testStartRuleDoc
+    , testStartRuleCode
+    , testCodeDoc
+    , testCodeCodeBlanksCode
+    , testCodeCodeBlanksDoc
+    , testCodeDocBlanksDoc
+    , testCodeDocBlanksCode
+    , testCodeRuleCode
+    , testCodeRuleDoc
+    , testDocCode
+    , testDocDocBlanksDoc
+    , testDocDocBlanksCode
+    , testDocCodeBlanksCode
+    , testDocCodeBlanksDoc
+    , testDocRuleDoc
+    , testDocRuleCode
+    , testEndDoc
+    , testEndDocBlanks
+    , testEndCode
+    , testEndCodeBlanks
+    , testEmpty
+    , testOnlyRule
+    , testOnlyDocBlanks
+    , testOnlyCodeBlanks
+    ]
diff --git a/test/LiterateX/Test/TargetFormat.hs b/test/LiterateX/Test/TargetFormat.hs
new file mode 100644
--- /dev/null
+++ b/test/LiterateX/Test/TargetFormat.hs
@@ -0,0 +1,167 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LiterateX.Test.TargetFormat (tests) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (TestTree, testGroup)
+
+-- https://hackage.haskell.org/package/tasty-hunit
+import Test.Tasty.HUnit ((@=?), testCase)
+
+-- https://hackage.haskell.org/package/text
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy (Text)
+
+-- (literatex)
+import qualified LiterateX
+import qualified LiterateX.Renderer as Renderer
+import qualified LiterateX.Types.SourceFormat as SourceFormat
+import qualified LiterateX.Types.TargetFormat as TargetFormat
+
+------------------------------------------------------------------------------
+
+sourceTL :: Text
+sourceTL = TL.unlines
+    [ "#!/usr/bin/env python"
+    , ""
+    , "# Test"
+    , ""
+    , "print('Hello!')"
+    ]
+
+------------------------------------------------------------------------------
+
+pandocLangNum :: Text
+pandocLangNum = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.python .numberSource startFrom=\"5\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+pandocLangNoNum :: Text
+pandocLangNoNum = TL.unlines
+    [ "Test"
+    , ""
+    , "```python"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+pandocNoLangNum :: Text
+pandocNoLangNum = TL.unlines
+    [ "Test"
+    , ""
+    , "``` {.numberSource startFrom=\"5\"}"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+pandocNoLangNoNum :: Text
+pandocNoLangNoNum = TL.unlines
+    [ "Test"
+    , ""
+    , "```"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+------------------------------------------------------------------------------
+
+githubLangNum :: Text
+githubLangNum = TL.unlines
+    [ "Test"
+    , ""
+    , "``` python startline=5"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+githubLangNoNum :: Text
+githubLangNoNum = TL.unlines
+    [ "Test"
+    , ""
+    , "``` python"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+githubNoLangNum :: Text
+githubNoLangNum = TL.unlines
+    [ "Test"
+    , ""
+    , "``` startline=5"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+githubNoLangNoNum :: Text
+githubNoLangNoNum = TL.unlines
+    [ "Test"
+    , ""
+    , "```"
+    , "print('Hello!')"
+    , "```"
+    ]
+
+------------------------------------------------------------------------------
+
+rendererOpts :: Renderer.Options
+rendererOpts = Renderer.Options
+    { Renderer.targetFormat    = TargetFormat.PandocMarkdown
+    , Renderer.codeLanguage    = Just "python"
+    , Renderer.ignoreShebang   = True
+    , Renderer.renderCode      = True
+    , Renderer.numberCodeLines = True
+    }
+
+rendererOptsGFM :: Renderer.Options
+rendererOptsGFM = rendererOpts
+    { Renderer.targetFormat = TargetFormat.GitHubFlavoredMarkdown
+    }
+
+run' :: Renderer.Options -> Text
+run' opts = LiterateX.transformTextToText SourceFormat.Hash opts sourceTL
+
+------------------------------------------------------------------------------
+
+testPandocMarkdown :: TestTree
+testPandocMarkdown = testGroup "PandocMarkdown"
+    [ testCase "LangNum" $ pandocLangNum @=? run' rendererOpts
+    , testCase "LangNoNum" $ pandocLangNoNum @=? run' rendererOpts
+        { Renderer.numberCodeLines = False
+        }
+    , testCase "NoLangNum" $ pandocNoLangNum @=? run' rendererOpts
+        { Renderer.codeLanguage = Nothing
+        }
+    , testCase "NoLangNoNum" $ pandocNoLangNoNum @=? run' rendererOpts
+        { Renderer.codeLanguage = Nothing
+        , Renderer.numberCodeLines = False
+        }
+    ]
+
+------------------------------------------------------------------------------
+
+testGitHubFlavoredMarkdown :: TestTree
+testGitHubFlavoredMarkdown = testGroup "GitHubFlavoredMarkdown"
+    [ testCase "LangNum" $ githubLangNum @=? run' rendererOptsGFM
+    , testCase "LangNoNum" $ githubLangNoNum @=? run' rendererOptsGFM
+        { Renderer.numberCodeLines = False
+        }
+    , testCase "NoLangNum" $ githubNoLangNum @=? run' rendererOptsGFM
+        { Renderer.codeLanguage = Nothing
+        }
+    , testCase "NoLangNoNum" $ githubNoLangNoNum @=? run' rendererOptsGFM
+        { Renderer.codeLanguage = Nothing
+        , Renderer.numberCodeLines = False
+        }
+    ]
+
+------------------------------------------------------------------------------
+
+tests :: TestTree
+tests = testGroup "LiterateX.Test.TargetFormat"
+    [ testPandocMarkdown
+    , testGitHubFlavoredMarkdown
+    ]
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,18 @@
+module Main (main) where
+
+-- https://hackage.haskell.org/package/tasty
+import Test.Tasty (defaultMain, testGroup)
+
+-- (literatex)
+import qualified LiterateX.Test.API
+import qualified LiterateX.Test.SourceFormat
+import qualified LiterateX.Test.TargetFormat
+
+------------------------------------------------------------------------------
+
+main :: IO ()
+main = defaultMain $ testGroup "test"
+    [ LiterateX.Test.API.tests
+    , LiterateX.Test.SourceFormat.tests
+    , LiterateX.Test.TargetFormat.tests
+    ]
