diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+5.3.0:
+    * Handle multiple deriving clauses in a DerivingStrategies scenario
+    * Ignore non-files in findCabalFiles
+    * Allow batch processing of multiple files
+    * Prevent hindent from trying to open non-files when searching for
+      .cabal files
+    * Specify default extensions in configuration file
+    * Fix bad output for [p|Foo|] pattern quasi-quotes
+    * Parse C preprocessor line continuations
+    * Fix pretty printing of '(:)
+    * Add parens around symbols (:|) when required
+    * Support $p pattern splices
+    * Fix associated type families
+    * Non-dependent record constructor formatting
+
 5.2.7:
     * Fix -X option bug
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# hindent [![Hackage](https://img.shields.io/hackage/v/hindent.svg?style=flat)](https://hackage.haskell.org/package/hindent) [![Build Status](https://travis-ci.org/commercialhaskell/hindent.png)](https://travis-ci.org/commercialhaskell/hindent)
+# hindent [![Hackage](https://img.shields.io/hackage/v/hindent.svg?style=flat)](https://hackage.haskell.org/package/hindent) [![Build Status](https://travis-ci.org/commercialhaskell/hindent.svg)](https://travis-ci.org/commercialhaskell/hindent)
 
 Haskell pretty printer
 
diff --git a/TESTS.md b/TESTS.md
--- a/TESTS.md
+++ b/TESTS.md
@@ -9,6 +9,19 @@
 
 # Modules
 
+Empty module
+
+``` haskell
+```
+
+Double shebangs
+
+``` haskell
+#!/usr/bin/env stack
+#!/usr/bin/env stack
+main = pure ()
+```
+
 Extension pragmas
 
 ```haskell
@@ -144,6 +157,14 @@
     k p
 ```
 
+Symbol class constructor in instance declaration
+
+```haskell
+instance Bool :?: Bool
+
+instance (:?:) Int Bool
+```
+
 GADT declarations
 
 ```haskell
@@ -247,6 +268,20 @@
     }
 ```
 
+Record with symbol constructor
+
+```haskell
+f = (:..?) {}
+```
+
+Record with symbol field
+
+```haskell
+f x = x {(..?) = wat}
+
+g x = Rec {(..?)}
+```
+
 Cases
 
 ``` haskell
@@ -299,6 +334,40 @@
   Left x -> x
 ```
 
+Operator in parentheses
+
+```haskell
+cat = (++)
+```
+
+Symbol data constructor in parentheses
+
+```haskell
+cons = (:)
+
+cons' = (:|)
+```
+
+n+k patterns
+
+``` haskell
+f (n+5) = 0
+```
+
+Binary symbol data constructor in pattern
+
+```haskell
+f (x :| _) = x
+
+f' ((:|) x _) = x
+
+f'' ((Data.List.NonEmpty.:|) x _) = x
+
+g (x:xs) = x
+
+g' ((:) x _) = x
+```
+
 Type application
 
 ```haskell
@@ -382,6 +451,23 @@
 foo :: $([t|Bool|]) -> a
 ```
 
+Quoted data constructors
+
+```haskell
+cons = '(:)
+```
+
+Pattern splices
+
+```haskell
+f $pat = ()
+
+g =
+  case x of
+    $(mkPat y z) -> True
+    _ -> False
+```
+
 # Type signatures
 
 Long argument list should line break
@@ -409,6 +495,13 @@
 fun :: (Class a, Class b) => a -> b -> c
 ```
 
+Symbol class constructor in class constraint
+
+```haskell
+f :: (a :?: b) => (a, b)
+f' :: ((:?:) a b) => (a, b)
+```
+
 Tuples
 
 ``` haskell
@@ -438,6 +531,13 @@
 f :: (?x :: Int) => Int
 ```
 
+Symbol type constructor
+
+```haskell
+f :: a :?: b
+f' :: (:?:) a b
+```
+
 Promoted list (issue #348)
 
 ```haskell
@@ -460,6 +560,15 @@
 b = undefined
 ```
 
+Prefix promoted symbol type constructor
+
+```haskell
+a :: '(T.:->) 'True 'False
+b :: (T.:->) 'True 'False
+c :: '(:->) 'True 'False
+d :: (:->) 'True 'False
+```
+
 # Function declarations
 
 Prefix notation for operators
@@ -614,6 +723,37 @@
   beta + delta + zeta + theta + kappa + mu + beta + delta + zeta + theta + kappa
 ```
 
+Symbol constructor, short
+
+```haskell
+fun ((:..?) {}) = undefined
+```
+
+Symbol constructor, long
+
+```
+fun (:..?) { alpha = beta
+           , gamma = delta
+           , epsilon = zeta
+           , eta = theta
+           , iota = kappa
+           , lambda = mu
+           } =
+  beta + delta + zeta + theta + kappa + mu + beta + delta + zeta + theta + kappa
+```
+
+Symbol field
+
+```haskell
+f (X {(..?) = x}) = x
+```
+
+Punned symbol field
+
+```haskell
+f' (X {(..?)}) = (..?)
+```
+
 # Johan Tibell compatibility checks
 
 Basic example from Tibbe's style
@@ -637,31 +777,64 @@
 
 ``` haskell
 data Tree a
-  = Branch !a
-           !(Tree a)
-           !(Tree a)
+  = Branch !a !(Tree a) !(Tree a)
   | Leaf
 
+data Tree a
+  = Branch
+      !a
+      !(Tree a)
+      !(Tree a)
+      !(Tree a)
+      !(Tree a)
+      !(Tree a)
+      !(Tree a)
+      !(Tree a)
+  | Leaf
+
 data HttpException
   = InvalidStatusCode Int
   | MissingContentHeader
 
-data Person = Person
-  { firstName :: !String -- ^ First name
-  , lastName :: !String -- ^ Last name
-  , age :: !Int -- ^ Age
-  }
+data Person =
+  Person
+    { firstName :: !String -- ^ First name
+    , lastName :: !String -- ^ Last name
+    , age :: !Int -- ^ Age
+    }
+
+data Expression a
+  = VariableExpression
+      { id :: Id Expression
+      , label :: a
+      }
+  | FunctionExpression
+      { var :: Id Expression
+      , body :: Expression a
+      , label :: a
+      }
+  | ApplyExpression
+      { func :: Expression a
+      , arg :: Expression a
+      , label :: a
+      }
+  | ConstructorExpression
+      { id :: Id Constructor
+      , label :: a
+      }
 ```
 
 Spaces between deriving classes
 
 ``` haskell
 -- From https://github.com/chrisdone/hindent/issues/167
-data Person = Person
-  { firstName :: !String -- ^ First name
-  , lastName :: !String -- ^ Last name
-  , age :: !Int -- ^ Age
-  } deriving (Eq, Show)
+data Person =
+  Person
+    { firstName :: !String -- ^ First name
+    , lastName :: !String -- ^ Last name
+    , age :: !Int -- ^ Age
+    }
+  deriving (Eq, Show)
 ```
 
 Hanging lambdas
@@ -768,18 +941,19 @@
   = X -- ^ X is for xylophone.
   | Y -- ^ Y is for why did I eat that pizza.
 
-data X = X
-  { field1 :: Int -- ^ Field1 is the first field.
-  , field11 :: Char
-    -- ^ This field comment is on its own line.
-  , field2 :: Int -- ^ Field2 is the second field.
-  , field3 :: Char -- ^ This is a long comment which starts next to
-    -- the field but continues onto the next line, it aligns exactly
-    -- with the field name.
-  , field4 :: Char
-    -- ^ This is a long comment which starts on the following line
-    -- from from the field, lines continue at the sme column.
-  }
+data X =
+  X
+    { field1 :: Int -- ^ Field1 is the first field.
+    , field11 :: Char
+      -- ^ This field comment is on its own line.
+    , field2 :: Int -- ^ Field2 is the second field.
+    , field3 :: Char -- ^ This is a long comment which starts next to
+      -- the field but continues onto the next line, it aligns exactly
+      -- with the field name.
+    , field4 :: Char
+      -- ^ This is a long comment which starts on the following line
+      -- from from the field, lines continue at the sme column.
+    }
 ```
 
 Comments around regular declarations
@@ -847,6 +1021,145 @@
 {- This is another random comment. -}
 ```
 
+# MINIMAL pragma
+
+Monad example
+
+```haskell
+class A where
+  {-# MINIMAL return, ((>>=) | (join, fmap)) #-}
+```
+
+Very long names #310
+
+```haskell
+class A where
+  {-# MINIMAL averylongnamewithnoparticularmeaning
+            | ananotherverylongnamewithnomoremeaning #-}
+```
+
+# Behaviour checks
+
+Unicode
+
+``` haskell
+α = γ * "ω"
+-- υ
+```
+
+Empty module
+
+``` haskell
+```
+
+Trailing newline is preserved
+
+``` haskell
+module X where
+
+foo = 123
+```
+
+# Complex input
+
+A complex, slow-to-print decl
+
+``` haskell
+quasiQuotes =
+  [ ( ''[]
+    , \(typeVariable:_) _automaticPrinter ->
+        (let presentVar = varE (presentVarName typeVariable)
+          in lamE
+               [varP (presentVarName typeVariable)]
+               [|(let typeString = "[" ++ fst $(presentVar) ++ "]"
+                   in ( typeString
+                      , \xs ->
+                          case fst $(presentVar) of
+                            "GHC.Types.Char" ->
+                              ChoicePresentation
+                                "String"
+                                [ ( "String"
+                                  , StringPresentation
+                                      "String"
+                                      (concatMap
+                                         getCh
+                                         (map (snd $(presentVar)) xs)))
+                                , ( "List of characters"
+                                  , ListPresentation
+                                      typeString
+                                      (map (snd $(presentVar)) xs))
+                                ]
+                              where getCh (CharPresentation "GHC.Types.Char" ch) =
+                                      ch
+                                    getCh (ChoicePresentation _ ((_, CharPresentation _ ch):_)) =
+                                      ch
+                                    getCh _ = ""
+                            _ ->
+                              ListPresentation
+                                typeString
+                                (map (snd $(presentVar)) xs)))|]))
+  ]
+```
+
+Random snippet from hindent itself
+
+``` haskell
+exp' (App _ op a) = do
+  (fits, st) <- fitsOnOneLine (spaced (map pretty (f : args)))
+  if fits
+    then put st
+    else do
+      pretty f
+      newline
+      spaces <- getIndentSpaces
+      indented spaces (lined (map pretty args))
+  where
+    (f, args) = flatten op [a]
+    flatten :: Exp NodeInfo -> [Exp NodeInfo] -> (Exp NodeInfo, [Exp NodeInfo])
+    flatten (App _ f' a') b = flatten f' (a' : b)
+    flatten f' as = (f', as)
+```
+
+Quasi quotes
+
+```haskell
+exp = [name|exp|]
+
+f [qq|pattern|] = ()
+```
+
+# C preprocessor
+
+Conditionals (`#if`)
+
+```haskell
+isDebug :: Bool
+#if DEBUG
+isDebug = True
+#else
+isDebug = False
+#endif
+```
+
+Macro definitions (`#define`)
+
+```haskell
+#define STRINGIFY(x) #x
+f = STRINGIFY (y)
+```
+
+Escaped newlines
+
+```haskell
+#define LONG_MACRO_DEFINITION \
+  data Pair a b = Pair \
+    { first :: a \
+    , second :: b \
+    }
+#define SHORT_MACRO_DEFINITION \
+  x
+```
+
 # Regression tests
 
 jml Adds trailing whitespace when wrapping #221
@@ -1085,8 +1398,26 @@
       }
 ```
 
-# MINIMAL pragma
+paraseba Deriving strategies with multiple deriving clauses
+```haskell
+-- https://github.com/commercialhaskell/hindent/issues/503
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
+module Foo where
+
+import Data.Typeable
+import GHC.Generics
+
+newtype Number a =
+  Number a
+  deriving (Generic)
+  deriving newtype (Show, Eq)
+  deriving anyclass (Typeable)
+```
+
 neongreen "{" is lost when formatting "Foo{}" #366
 
 ```haskell
@@ -1131,17 +1462,22 @@
 ```haskell
 -- https://github.com/commercialhaskell/hindent/issues/393
 data X
-  = X { x :: Int }
+  = X
+      { x :: Int
+      }
   | X'
 
-data X = X
-  { x :: Int
-  , x' :: Int
-  }
+data X =
+  X
+    { x :: Int
+    , x' :: Int
+    }
 
 data X
-  = X { x :: Int
-      , x' :: Int }
+  = X
+      { x :: Int
+      , x' :: Int
+      }
   | X'
 ```
 
@@ -1353,9 +1689,7 @@
 {-# LANGUAGE ExistentialQuantification #-}
 
 data D =
-  forall a b c. D a
-                  b
-                  c
+  forall a b c. D a b c
 ```
 
 sophie-h Regression: Breaks basic type class code by inserting "|" #459
@@ -1428,107 +1762,27 @@
 import qualified "base" Prelude as P
 ```
 
-# MINIMAL pragma
-
-Monad example
+alexwl Hindent breaks associated type families annotated with injectivity information #528
 
 ```haskell
-class A where
-  {-# MINIMAL return, ((>>=) | (join, fmap)) #-}
+-- https://github.com/commercialhaskell/hindent/issues/528
+class C a where
+  type F a = b | b -> a
 ```
 
-Very long names #310
+sophie-h Fails to create required indentation for infix #238
 
 ```haskell
-class A where
-  {-# MINIMAL averylongnamewithnoparticularmeaning
-            | ananotherverylongnamewithnomoremeaning #-}
-```
-
-# Behaviour checks
-
-Unicode
-
-``` haskell
-α = γ * "ω"
--- υ
-```
-
-Empty module
-
-``` haskell
-```
-
-Trailing newline is preserved
-
-``` haskell
-module X where
-
-foo = 123
-```
-
-# Complex input
-
-A complex, slow-to-print decl
-
-``` haskell
-quasiQuotes =
-  [ ( ''[]
-    , \(typeVariable:_) _automaticPrinter ->
-        (let presentVar = varE (presentVarName typeVariable)
-          in lamE
-               [varP (presentVarName typeVariable)]
-               [|(let typeString = "[" ++ fst $(presentVar) ++ "]"
-                   in ( typeString
-                      , \xs ->
-                          case fst $(presentVar) of
-                            "GHC.Types.Char" ->
-                              ChoicePresentation
-                                "String"
-                                [ ( "String"
-                                  , StringPresentation
-                                      "String"
-                                      (concatMap
-                                         getCh
-                                         (map (snd $(presentVar)) xs)))
-                                , ( "List of characters"
-                                  , ListPresentation
-                                      typeString
-                                      (map (snd $(presentVar)) xs))
-                                ]
-                              where getCh (CharPresentation "GHC.Types.Char" ch) =
-                                      ch
-                                    getCh (ChoicePresentation _ ((_, CharPresentation _ ch):_)) =
-                                      ch
-                                    getCh _ = ""
-                            _ ->
-                              ListPresentation
-                                typeString
-                                (map (snd $(presentVar)) xs)))|]))
-  ]
-```
-
-Random snippet from hindent itself
+-- https://github.com/commercialhaskell/hindent/issues/238
+{-# LANGUAGE ScopedTypeVariables #-}
 
-``` haskell
-exp' (App _ op a) = do
-  (fits, st) <- fitsOnOneLine (spaced (map pretty (f : args)))
-  if fits
-    then put st
-    else do
-      pretty f
-      newline
-      spaces <- getIndentSpaces
-      indented spaces (lined (map pretty args))
-  where
-    (f, args) = flatten op [a]
-    flatten :: Exp NodeInfo -> [Exp NodeInfo] -> (Exp NodeInfo, [Exp NodeInfo])
-    flatten (App _ f' a') b = flatten f' (a' : b)
-    flatten f' as = (f', as)
-```
+import Control.Exception
 
-Quasi quotes
+x :: IO Int
+x =
+  do putStrLn "ok"
+     error "ok"
+     `catch` (\(_ :: IOException) -> pure 1) `catch`
+  (\(_ :: ErrorCall) -> pure 2)
 
-```haskell
-exp = [name|exp|]
 ```
diff --git a/hindent.cabal b/hindent.cabal
--- a/hindent.cabal
+++ b/hindent.cabal
@@ -1,5 +1,5 @@
 name:                hindent
-version:             5.2.7
+version:             5.3.0
 synopsis:            Extensible Haskell pretty printer
 description:         Extensible Haskell pretty printer. Both a library and an executable.
                      .
@@ -33,6 +33,7 @@
                      HIndent.Types
                      HIndent.Pretty
                      HIndent.CabalFile
+                     HIndent.CodeBlock
   build-depends:     base >= 4.7 && <5
                    , containers
                    , Cabal
@@ -105,3 +106,4 @@
                    , criterion
                    , deepseq
                    , exceptions
+                   , mtl
diff --git a/src/HIndent.hs b/src/HIndent.hs
--- a/src/HIndent.hs
+++ b/src/HIndent.hs
@@ -41,20 +41,13 @@
 import           Data.Text (Text)
 import qualified Data.Text as T
 import           Data.Traversable hiding (mapM)
+import           HIndent.CodeBlock
 import           HIndent.Pretty
 import           HIndent.Types
 import qualified Language.Haskell.Exts as Exts
 import           Language.Haskell.Exts hiding (Style, prettyPrint, Pretty, style, parse)
 import           Prelude
 
--- | A block of code.
-data CodeBlock
-    = Shebang ByteString
-    | HaskellSource Int ByteString
-    -- ^ Includes the starting line (indexed from 0) for error reporting
-    | CPPDirectives ByteString
-     deriving (Show, Eq)
-
 -- | Format the given source.
 reformat :: Config -> Maybe [Extension] -> Maybe FilePath -> ByteString -> Either String Builder
 reformat config mexts mfilepath =
@@ -72,10 +65,16 @@
             mode'' = case exts of
                        Nothing -> mode'
                        Just (Nothing, exts') ->
-                         mode' { extensions = exts' ++ extensions mode' }
+                         mode' { extensions =
+                                   exts'
+                                   ++ configExtensions config
+                                   ++ extensions mode' }
                        Just (Just lang, exts') ->
                          mode' { baseLanguage = lang
-                               , extensions = exts' ++ extensions mode' }
+                               , extensions =
+                                   exts'
+                                   ++ configExtensions config
+                                   ++ extensions mode' }
         in case parseModuleWithComments mode'' (UTF8.toString code) of
                ParseOk (m, comments) ->
                    fmap
@@ -148,62 +147,6 @@
         then False
         else S8.last xs == '\n'
 
--- | Break a Haskell code string into chunks, using CPP as a delimiter.
--- Lines that start with '#if', '#end', or '#else' are their own chunks, and
--- also act as chunk separators. For example, the code
---
--- > #ifdef X
--- > x = X
--- > y = Y
--- > #else
--- > x = Y
--- > y = X
--- > #endif
---
--- will become five blocks, one for each CPP line and one for each pair of declarations.
-cppSplitBlocks :: ByteString -> [CodeBlock]
-cppSplitBlocks inp =
-  modifyLast (inBlock (<> trailing)) .
-  map (classify . unlines') .
-  groupBy ((==) `on` nonHaskellLine) . zip [0 ..] . S8.lines $
-  inp
-  where
-    nonHaskellLine :: (Int, ByteString) -> Bool
-    nonHaskellLine (_, src) = cppLine src || shebangLine src
-    shebangLine :: ByteString -> Bool
-    shebangLine = S8.isPrefixOf "#!"
-    cppLine :: ByteString -> Bool
-    cppLine src =
-      any
-        (`S8.isPrefixOf` src)
-        ["#if", "#end", "#else", "#define", "#undef", "#elif", "#include", "#error", "#warning"]
-        -- Note: #ifdef and #ifndef are handled by #if
-    unlines' :: [(Int, ByteString)] -> (Int, ByteString)
-    unlines' [] = (0, S.empty)
-    unlines' srcs@((line, _):_) =
-      (line, mconcat . intersperse "\n" $ map snd srcs)
-    classify :: (Int, ByteString) -> CodeBlock
-    classify (line, text)
-      | shebangLine text = Shebang text
-      | cppLine text = CPPDirectives text
-      | otherwise = HaskellSource line text
-    -- Hack to work around some parser issues in haskell-src-exts: Some pragmas
-    -- need to have a newline following them in order to parse properly, so we include
-    -- the trailing newline in the code block if it existed.
-    trailing :: ByteString
-    trailing =
-      if S8.isSuffixOf "\n" inp
-        then "\n"
-        else ""
-    modifyLast :: (a -> a) -> [a] -> [a]
-    modifyLast _ [] = []
-    modifyLast f [x] = [f x]
-    modifyLast f (x:xs) = x : modifyLast f xs
-    inBlock :: (ByteString -> ByteString) -> CodeBlock -> CodeBlock
-    inBlock f (HaskellSource line txt) = HaskellSource line (f txt)
-    inBlock _ dir = dir
-
-
 -- | Print the module.
 prettyPrint :: Config
             -> Module SrcSpanInfo
@@ -319,14 +262,6 @@
             x' :
             delete x' a
         f _ x = error $ "Unknown extension: " ++ x
-
--- | Parse an extension.
-readExtension :: String -> Maybe Extension
-readExtension x =
-  case classifyExtension x -- Foo
-       of
-    UnknownExtension _ -> Nothing
-    x' -> Just x'
 
 --------------------------------------------------------------------------------
 -- Comments
diff --git a/src/HIndent/CabalFile.hs b/src/HIndent/CabalFile.hs
--- a/src/HIndent/CabalFile.hs
+++ b/src/HIndent/CabalFile.hs
@@ -4,6 +4,7 @@
   ( getCabalExtensionsForSourcePath
   ) where
 
+import Control.Monad
 import qualified Data.ByteString as BS
 import Data.List
 import Data.Maybe
@@ -82,7 +83,8 @@
 findCabalFiles :: FilePath -> FilePath -> IO (Maybe ([FilePath], FilePath))
 findCabalFiles dir rel = do
   names <- getDirectoryContents dir
-  let cabalnames = filter (isSuffixOf ".cabal") names
+  cabalnames <-
+    filterM (doesFileExist . (dir </>)) $ filter (isSuffixOf ".cabal") names
   case cabalnames of
     []
       | dir == "/" -> return Nothing
diff --git a/src/HIndent/CodeBlock.hs b/src/HIndent/CodeBlock.hs
new file mode 100644
--- /dev/null
+++ b/src/HIndent/CodeBlock.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module HIndent.CodeBlock
+  ( CodeBlock(..)
+  , cppSplitBlocks
+  ) where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Char8 as S8
+import Data.Monoid
+
+-- | A block of code.
+data CodeBlock
+    = Shebang ByteString
+    | HaskellSource Int ByteString
+    -- ^ Includes the starting line (indexed from 0) for error reporting
+    | CPPDirectives ByteString
+     deriving (Show, Eq)
+
+-- | Break a Haskell code string into chunks, using CPP as a delimiter.
+-- Lines that start with '#if', '#end', or '#else' are their own chunks, and
+-- also act as chunk separators. For example, the code
+--
+-- > #ifdef X
+-- > x = X
+-- > y = Y
+-- > #else
+-- > x = Y
+-- > y = X
+-- > #endif
+--
+-- will become five blocks, one for each CPP line and one for each pair of declarations.
+cppSplitBlocks :: ByteString -> [CodeBlock]
+cppSplitBlocks inp =
+  modifyLast (inBlock (<> trailing)) .
+  groupLines . classifyLines . zip [0 ..] . S8.lines $
+  inp
+  where
+    groupLines :: [CodeBlock] -> [CodeBlock]
+    groupLines (line1:line2:remainingLines) =
+      case mergeLines line1 line2 of
+        Just line1And2 -> groupLines (line1And2 : remainingLines)
+        Nothing -> line1 : groupLines (line2 : remainingLines)
+    groupLines xs@[_] = xs
+    groupLines xs@[] = xs
+    mergeLines :: CodeBlock -> CodeBlock -> Maybe CodeBlock
+    mergeLines (CPPDirectives src1) (CPPDirectives src2) =
+      Just $ CPPDirectives (src1 <> "\n" <> src2)
+    mergeLines (Shebang src1) (Shebang src2) =
+      Just $ Shebang (src1 <> "\n" <> src2)
+    mergeLines (HaskellSource lineNumber1 src1) (HaskellSource _lineNumber2 src2) =
+      Just $ HaskellSource lineNumber1 (src1 <> "\n" <> src2)
+    mergeLines _ _ = Nothing
+    shebangLine :: ByteString -> Bool
+    shebangLine = S8.isPrefixOf "#!"
+    cppLine :: ByteString -> Bool
+    cppLine src =
+      any
+        (`S8.isPrefixOf` src)
+        ["#if", "#end", "#else", "#define", "#undef", "#elif", "#include", "#error", "#warning"]
+        -- Note: #ifdef and #ifndef are handled by #if
+    hasEscapedTrailingNewline :: ByteString -> Bool
+    hasEscapedTrailingNewline src = "\\" `S8.isSuffixOf` src
+    classifyLines :: [(Int, ByteString)] -> [CodeBlock]
+    classifyLines allLines@((lineIndex, src):nextLines)
+      | cppLine src =
+        let (cppLines, nextLines') = spanCPPLines allLines
+         in CPPDirectives (S8.intercalate "\n" (map snd cppLines)) :
+            classifyLines nextLines'
+      | shebangLine src = Shebang src : classifyLines nextLines
+      | otherwise = HaskellSource lineIndex src : classifyLines nextLines
+    classifyLines [] = []
+    spanCPPLines ::
+         [(Int, ByteString)] -> ([(Int, ByteString)], [(Int, ByteString)])
+    spanCPPLines (line@(_, src):nextLines)
+      | hasEscapedTrailingNewline src =
+        let (cppLines, nextLines') = spanCPPLines nextLines
+         in (line : cppLines, nextLines')
+      | otherwise = ([line], nextLines)
+    spanCPPLines [] = ([], [])
+    -- Hack to work around some parser issues in haskell-src-exts: Some pragmas
+    -- need to have a newline following them in order to parse properly, so we include
+    -- the trailing newline in the code block if it existed.
+    trailing :: ByteString
+    trailing =
+      if S8.isSuffixOf "\n" inp
+        then "\n"
+        else ""
+    modifyLast :: (a -> a) -> [a] -> [a]
+    modifyLast _ [] = []
+    modifyLast f [x] = [f x]
+    modifyLast f (x:xs) = x : modifyLast f xs
+    inBlock :: (ByteString -> ByteString) -> CodeBlock -> CodeBlock
+    inBlock f (HaskellSource line txt) = HaskellSource line (f txt)
+    inBlock _ dir = dir
diff --git a/src/HIndent/Pretty.hs b/src/HIndent/Pretty.hs
--- a/src/HIndent/Pretty.hs
+++ b/src/HIndent/Pretty.hs
@@ -16,7 +16,7 @@
 import           Control.Applicative
 import           Control.Monad.State.Strict hiding (state)
 import qualified Data.ByteString.Builder as S
-import           Data.Foldable (for_, traverse_)
+import           Data.Foldable (for_, forM_, traverse_)
 import           Data.Int
 import           Data.List
 import           Data.Maybe
@@ -400,10 +400,7 @@
         depend (do pretty e
                    write " -> ")
                (pretty p)
-      PQuasiQuote _ name str ->
-        brackets (depend (do string name
-                             write "|")
-                         (string str))
+      PQuasiQuote _ name str -> quotation name (string str)
       PBangPat _ p ->
         depend (write "!")
                (pretty p)
@@ -414,6 +411,7 @@
       PXPatTag{} -> pretty' x
       PXRPats{} -> pretty' x
       PVar{} -> pretty' x
+      PSplice _ s -> pretty s
 
 -- | Pretty infix application of a name (identifier or symbol).
 prettyInfixName :: Name NodeInfo -> Printer ()
@@ -437,19 +435,6 @@
     Ident _ i -> string i
     Symbol _ s -> string ("(" ++ s ++ ")")
 
-prettyQuoteQName :: QName NodeInfo -> Printer ()
-prettyQuoteQName x =
-  case x of
-    Qual _ mn n ->
-      case n of
-        Ident _ i -> do pretty mn; write "."; string i;
-        Symbol _ s -> do write "("; pretty mn; write "."; string s; write ")";
-    UnQual _ n ->
-      case n of
-        Ident _ i -> string i
-        Symbol _ s -> do write "("; string s; write ")";
-    Special _ s -> pretty s
-
 instance Pretty Type where
   prettyInternal = typ
 
@@ -666,17 +651,13 @@
          (pretty t)
 exp (VarQuote _ x) =
   depend (write "'")
-         (prettyQuoteQName x)
+         (pretty x)
 exp (TypQuote _ x) =
   depend (write "''")
-         (prettyQuoteQName x)
+         (pretty x)
 exp (BracketExp _ b) = pretty b
 exp (SpliceExp _ s) = pretty s
-exp (QuasiQuote _ n s) =
-  brackets (depend (do string n
-                       write "|")
-                   (do string s
-                       write "|"))
+exp (QuasiQuote _ n s) = quotation n (string s)
 exp (LCase _ alts) =
   do write "\\case"
      if null alts
@@ -708,17 +689,9 @@
                          (zip [1..] stmts))))
       swing (write " " >> rhsSeparator) (pretty e)
 exp (Lit _ lit) = prettyInternal lit
-exp (Var _ q) = case q of
-                  Special _ Cons{} -> parens (pretty q)
-                  Qual _ _ (Symbol _ _) -> parens (pretty q)
-                  UnQual _ (Symbol _ _) -> parens (pretty q)
-                  _ -> pretty q
+exp (Var _ q) = pretty q
 exp (IPVar _ q) = pretty q
-exp (Con _ q) = case q of
-                  Special _ Cons{} -> parens (pretty q)
-                  Qual _ _ (Symbol _ _) -> parens (pretty q)
-                  UnQual _ (Symbol _ _) -> parens (pretty q)
-                  _ -> pretty q
+exp (Con _ q) = pretty q
 
 exp x@XTag{} = pretty' x
 exp x@XETag{} = pretty' x
@@ -850,11 +823,7 @@
                            [x] -> singleCons x
                            xs -> multiCons xs))
      indentSpaces <- getIndentSpaces
-     case mderivs of
-       [] -> return ()
-       [derivs] ->
-         do newline
-            column indentSpaces (pretty derivs)
+     forM_ mderivs $ \deriv -> newline >> column indentSpaces (pretty deriv)
   where singleCons x =
           do write " ="
              indentSpaces <- getIndentSpaces
@@ -884,10 +853,7 @@
          _ -> do
            newline
            lined (map pretty condecls)
-       case mderivs of
-         [] -> return ()
-         [derivs] -> do newline
-                        pretty derivs
+       forM_ mderivs $ \deriv -> newline >> pretty deriv
 
 decl (InlineSig _ inline active name) = do
   write "{-# "
@@ -898,7 +864,7 @@
     Nothing -> return ()
     Just (ActiveFrom _ x) -> write ("[" ++ show x ++ "] ")
     Just (ActiveUntil _ x) -> write ("[~" ++ show x ++ "] ")
-  prettyQuoteQName name
+  pretty name
 
   write " #-}"
 decl (MinimalPragma _ (Just formula)) =
@@ -969,8 +935,8 @@
     pretty out_
 
 instance Pretty Deriving where
-  prettyInternal (Deriving _ _ heads) =
-    depend (write "deriving" >> space) $ do
+  prettyInternal (Deriving _ strategy heads) =
+    depend (write "deriving" >> space >> writeStrategy) $ do
       let heads' =
             if length heads == 1
               then map stripParens heads
@@ -980,6 +946,9 @@
         Nothing -> formatMultiLine heads'
         Just derives -> put derives
     where
+      writeStrategy = case strategy of
+        Nothing -> return ()
+        Just st -> pretty st >> space
       stripParens (IParen _ iRule) = stripParens iRule
       stripParens x = x
       formatMultiLine derives = do
@@ -987,6 +956,13 @@
         newline
         write ")"
 
+instance Pretty DerivStrategy where
+  prettyInternal x =
+    case x of
+      DerivStock _ -> return ()
+      DerivAnyclass _ -> write "anyclass"
+      DerivNewtype _ -> write "newtype"
+
 instance Pretty Alt where
   prettyInternal x =
     case x of
@@ -1046,29 +1022,38 @@
     case x of
       ClsDecl _ d -> pretty d
       ClsDataFam _ ctx h mkind ->
-        depend (write "data ")
-               (withCtx ctx
-                        (do pretty h
-                            (case mkind of
-                               Nothing -> return ()
-                               Just kind ->
-                                 do write " :: "
-                                    pretty kind)))
-      ClsTyFam _ h mkind minj ->
-        depend (write "type ")
-               (depend (pretty h)
-                       (depend (traverse_ (\kind -> write " :: " >> pretty kind) mkind)
-                               (traverse_ pretty minj)))
-      ClsTyDef _ (TypeEqn _ this that) ->
-        do write "type "
-           pretty this
-           write " = "
-           pretty that
-      ClsDefSig _ name ty ->
-        do write "default "
-           pretty name
-           write " :: "
-           pretty ty
+        depend
+          (write "data ")
+          (withCtx
+             ctx
+             (do pretty h
+                 (case mkind of
+                    Nothing -> return ()
+                    Just kind -> do
+                      write " :: "
+                      pretty kind)))
+      ClsTyFam _ h msig minj ->
+        depend
+          (write "type ")
+          (depend
+             (pretty h)
+             (depend
+                (traverse_
+                   (\case
+                      KindSig _ kind -> write " :: " >> pretty kind
+                      TyVarSig _ tyVarBind -> write " = " >> pretty tyVarBind)
+                   msig)
+                (traverse_ (\inj -> space >> pretty inj) minj)))
+      ClsTyDef _ (TypeEqn _ this that) -> do
+        write "type "
+        pretty this
+        write " = "
+        pretty that
+      ClsDefSig _ name ty -> do
+        write "default "
+        pretty name
+        write " :: "
+        pretty ty
 
 instance Pretty ConDecl where
   prettyInternal x =
@@ -1418,24 +1403,9 @@
 instance Pretty Bracket where
   prettyInternal x =
     case x of
-      ExpBracket _ p ->
-        brackets
-          (depend
-             (write "|")
-             (do pretty p
-                 write "|"))
-      PatBracket _ p ->
-        brackets
-          (depend
-             (write "p|")
-             (do pretty p
-                 write "|"))
-      TypeBracket _ ty ->
-        brackets
-          (depend
-             (write "t|")
-             (do pretty ty
-                 write "|"))
+      ExpBracket _ p -> quotation "" (pretty p)
+      PatBracket _ p -> quotation "p" (pretty p)
+      TypeBracket _ ty -> quotation "t" (pretty ty)
       d@(DeclBracket _ _) -> pretty' d
 
 instance Pretty IPBind where
@@ -1512,13 +1482,19 @@
 instance Pretty QName where
   prettyInternal =
     \case
-      Qual _ m n -> do
-        pretty m
-        write "."
-        pretty n
-      UnQual _ n -> pretty n
-      Special _ c -> pretty c
+      Qual _ mn n ->
+        case n of
+          Ident _ i -> do pretty mn; write "."; string i;
+          Symbol _ s -> do write "("; pretty mn; write "."; string s; write ")";
+      UnQual _ n ->
+        case n of
+          Ident _ i -> string i
+          Symbol _ s -> do write "("; string s; write ")";
+      Special _ s@Cons{} -> parens (pretty s)
+      Special _ s@FunCon{} -> parens (pretty s)
+      Special _ s -> pretty s
 
+
 instance Pretty SpecialCon where
   prettyInternal s =
     case s of
@@ -1803,20 +1779,7 @@
                write ":")
 typ (TyApp _ f a) = spaced [pretty f, pretty a]
 typ (TyVar _ n) = pretty n
-typ (TyCon _ p) =
-  case p of
-    Qual _ _ name ->
-      case name of
-        Ident _ _ -> pretty p
-        Symbol _ _ -> parens (pretty p)
-    UnQual _ name ->
-      case name of
-        Ident _ _ -> pretty p
-        Symbol _ _ -> parens (pretty p)
-    Special _ con ->
-      case con of
-        FunCon _ -> parens (pretty p)
-        _ -> pretty p
+typ (TyCon _ p) = pretty p
 typ (TyParen _ e) = parens (pretty e)
 typ (TyInfix _ a promotedop b) = do
   -- Apply special rules to line-break operators.
@@ -1877,11 +1840,7 @@
     Just n ->
       do write "_"
          pretty n
-typ (TyQuasiQuote _ n s) =
-  brackets (depend (do string n
-                       write "|")
-                   (do string s
-                       write "|"))
+typ (TyQuasiQuote _ n s) = quotation n (string s)
 typ (TyUnboxedSum{}) = error "FIXME: No implementation for TyUnboxedSum."
 
 prettyTopName :: Name NodeInfo -> Printer ()
@@ -1924,20 +1883,6 @@
        for_ mbinds bindingGroup
 
 -- | Handle records specially for a prettier display (see guide).
-decl' (DataDecl _ dataornew ctx dhead [con] mderivs)
-  | isRecord con =
-    do depend (do pretty dataornew
-                  space)
-              (withCtx ctx
-                       (do pretty dhead
-                           singleCons con))
-       case mderivs of
-         [] -> return ()
-         [derivs] -> do space
-                        pretty derivs
-  where singleCons x =
-          depend (write " =")
-                 ((depend space . qualConDecl) x)
 decl' e = decl e
 
 declTy :: Type NodeInfo -> Printer ()
@@ -2011,17 +1956,25 @@
 
 -- | Fields are preceded with a space.
 conDecl :: ConDecl NodeInfo -> Printer ()
-conDecl (RecDecl _ name fields) =
-  depend (do pretty name
-             write " ")
-         (do depend (write "{")
-                    (prefixedLined ","
-                                   (map (depend space . pretty) fields))
-             write " }")
-conDecl (ConDecl _ name bangty) =
-  depend (do prettyQuoteName name
-             unless (null bangty) space)
-         (lined (map pretty bangty))
+conDecl (RecDecl _ name fields) = do
+   pretty name
+   newline
+   indentedBlock
+    (do depend (write "{")
+               (prefixedLined ","
+                              (map (depend space . pretty) fields))
+        newline
+        write "}"
+        )
+conDecl (ConDecl _ name bangty) = do
+  prettyQuoteName name
+  unless
+    (null bangty)
+    (ifFitsOnOneLineOrElse
+       (do space
+           spaced (map pretty bangty))
+       (do newline
+           indentedBlock (lined (map pretty bangty))))
 conDecl (InfixConDecl _ a f b) =
   inter space [pretty a, pretty f, pretty b]
 
@@ -2034,7 +1987,7 @@
      indentSpaces <- getIndentSpaces
      newline
      column indentSpaces
-            (do depend (write "{")
+            (do depend (write "{!")
                        (prefixedLined ","
                                       (map (depend space . pretty) fields))
                 newline
@@ -2168,3 +2121,16 @@
   [OpChainLink op] <>
   flattenOpChain right
 flattenOpChain e = [OpChainExp e]
+
+-- | Write a Template Haskell quotation or a quasi-quotation.
+--
+-- >>> quotation "t" (string "Foo")
+-- > [t|Foo|]
+quotation :: String -> Printer () -> Printer ()
+quotation quoter body =
+  brackets
+    (depend
+       (do string quoter
+           write "|")
+       (do body
+           write "|"))
diff --git a/src/HIndent/Types.hs b/src/HIndent/Types.hs
--- a/src/HIndent/Types.hs
+++ b/src/HIndent/Types.hs
@@ -11,6 +11,7 @@
   (Printer(..)
   ,PrintState(..)
   ,Config(..)
+  ,readExtension
   ,defaultConfig
   ,NodeInfo(..)
   ,NodeComment(..)
@@ -27,7 +28,7 @@
 import           Data.Maybe
 import           Data.Yaml (FromJSON(..))
 import qualified Data.Yaml as Y
-import           Language.Haskell.Exts.SrcLoc
+import           Language.Haskell.Exts hiding (Style, prettyPrint, Pretty, style, parse)
 
 -- | A pretty printing monad.
 newtype Printer a =
@@ -64,8 +65,18 @@
     , configTrailingNewline :: !Bool -- ^ End with a newline.
     , configSortImports :: !Bool -- ^ Sort imports in groups.
     , configLineBreaks :: [String] -- ^ Break line when meets these operators.
+    , configExtensions :: [Extension]
+      -- ^ Extra language extensions enabled by default.
     }
 
+-- | Parse an extension.
+readExtension :: Monad m => String -> m Extension
+readExtension x =
+  case classifyExtension x -- Foo
+       of
+    UnknownExtension _ -> fail ("Unknown extension: " ++ x)
+    x' -> return x'
+
 instance FromJSON Config where
   parseJSON (Y.Object v) =
     Config <$>
@@ -83,7 +94,9 @@
         (v Y..:? "sort-imports") <*>
       fmap
         (fromMaybe (configLineBreaks defaultConfig))
-        (v Y..:? "line-breaks")
+        (v Y..:? "line-breaks") <*>
+      (traverse readExtension
+        =<< fmap (fromMaybe []) (v Y..:? "extensions"))
   parseJSON _ = fail "Expected Object for Config value"
 
 -- | Default style configuration.
@@ -95,6 +108,7 @@
     , configTrailingNewline = True
     , configSortImports = True
     , configLineBreaks = []
+    , configExtensions = []
     }
 
 -- | Some comment to print.
diff --git a/src/main/Main.hs b/src/main/Main.hs
--- a/src/main/Main.hs
+++ b/src/main/Main.hs
@@ -34,7 +34,7 @@
 
 data Action = Validate | Reformat
 
-data RunMode = ShowVersion | Run Config [Extension] Action (Maybe FilePath)
+data RunMode = ShowVersion | Run Config [Extension] Action [FilePath]
 
 -- | Main entry point.
 main :: IO ()
@@ -44,12 +44,15 @@
   case runMode of
     ShowVersion ->
       putStrLn ("hindent " ++ showVersion version)
-    Run style exts action mfilepath ->
-      case mfilepath of
-        Just filepath -> do
+    Run style exts action paths ->
+      if null paths then
+         L8.interact
+            (either error S.toLazyByteString . reformat style (Just exts) Nothing . L8.toStrict)
+      else
+        forM_ paths $ \filepath -> do
           cabalexts <- getCabalExtensionsForSourcePath filepath
           text <- S.readFile filepath
-          case reformat style (Just $ cabalexts ++ exts) mfilepath text of
+          case reformat style (Just $ cabalexts ++ exts) (Just filepath) text of
             Left e -> error e
             Right out ->
               unless (L8.fromStrict text == S.toLazyByteString out) $
@@ -69,9 +72,6 @@
                             else throw e
                     IO.copyPermissions filepath fp
                     IO.renameFile fp filepath `catch` exdev
-        Nothing ->
-          L8.interact
-            (either error S.toLazyByteString . reformat style (Just exts) Nothing . L8.toStrict)
 
 -- | Read config from a config file, or return 'defaultConfig'.
 getConfig :: IO Config
@@ -124,4 +124,4 @@
       , configTrailingNewline =  trailing
       , configSortImports =  fromMaybe (configSortImports s) imports
       }
-    files = optional (strArgument (metavar "FILENAME"))
+    files = many (strArgument (metavar "FILENAMES"))
diff --git a/src/main/Markdone.hs b/src/main/Markdone.hs
--- a/src/main/Markdone.hs
+++ b/src/main/Markdone.hs
@@ -13,6 +13,7 @@
 
 import           Control.DeepSeq
 import           Control.Monad.Catch
+import           Control.Monad.State.Strict (State, evalState, get, put)
 import           Data.ByteString (ByteString)
 import qualified Data.ByteString.Char8 as S8
 import           Data.Char
@@ -26,7 +27,7 @@
   | PlainLine !ByteString
   | BeginFence !ByteString
   | EndFence
-  deriving (Show)
+  deriving (Eq, Show)
 
 -- | A markdone document.
 data Markdone
@@ -35,7 +36,7 @@
   | CodeFence !ByteString
               !ByteString
   | PlainText !ByteString
-  deriving (Show,Generic)
+  deriving (Eq,Show,Generic)
 instance NFData Markdone
 
 -- | Parse error.
@@ -43,23 +44,39 @@
   deriving (Typeable,Show)
 instance Exception MarkdownError
 
+data TokenizerMode
+  = Normal
+  | Fenced
+
 -- | Tokenize the bytestring.
 tokenize :: ByteString -> [Token]
-tokenize = map token . S8.lines
+tokenize input = evalState (mapM token (S8.lines input)) Normal
   where
-    token line =
-      if S8.isPrefixOf "#" line && not (S8.isPrefixOf "#!" line)
-        then let (hashes,title) = S8.span (== '#') line
-             in Heading (S8.length hashes) (S8.dropWhile isSpace title)
-        else if S8.isPrefixOf "```" line
-               then if line == "```"
-                      then EndFence
-                      else BeginFence
-                             (S8.dropWhile
-                                (\c ->
-                                    c == '`' || c == ' ')
-                                line)
-               else PlainLine line
+    token :: ByteString -> State TokenizerMode Token
+    token line = do
+      mode <- get
+      case mode of
+        Normal ->
+          if S8.isPrefixOf "#" line
+            then let (hashes,title) = S8.span (== '#') line
+                 in return $
+                      Heading (S8.length hashes) (S8.dropWhile isSpace title)
+            else if S8.isPrefixOf "```" line
+                   then do
+                     put Fenced
+                     return $
+                       BeginFence
+                         (S8.dropWhile
+                            (\c ->
+                                c == '`' || c == ' ')
+                            line)
+                   else return $ PlainLine line
+        Fenced ->
+          if line == "```"
+            then do
+              put Normal
+              return EndFence
+            else return $ PlainLine line
 
 -- | Parse into a forest.
 parse :: (Functor m,MonadThrow m) => [Token] -> m [Markdone]
diff --git a/src/main/Test.hs b/src/main/Test.hs
--- a/src/main/Test.hs
+++ b/src/main/Test.hs
@@ -15,6 +15,7 @@
 import           Data.Function
 import           Data.Monoid
 import qualified HIndent
+import           HIndent.CodeBlock
 import           HIndent.Types
 import           Markdone
 import           Test.Hspec
@@ -24,7 +25,10 @@
 main = do
     bytes <- S.readFile "TESTS.md"
     forest <- parse (tokenize bytes)
-    hspec (toSpec forest)
+    hspec $ do
+      codeBlocksSpec
+      markdoneSpec
+      toSpec forest
 
 reformat :: Config -> S.ByteString -> ByteString
 reformat cfg code =
@@ -97,3 +101,77 @@
 skipEmptyLines :: [Markdone] -> [Markdone]
 skipEmptyLines (PlainText "":rest) = rest
 skipEmptyLines other = other
+
+codeBlocksSpec :: Spec
+codeBlocksSpec =
+  describe "splitting source into code blocks" $ do
+    it "should put just Haskell code in its own block" $ do
+      let input = "this is totally haskell code\n\nit deserves its own block!\n"
+      cppSplitBlocks input `shouldBe` [HaskellSource 0 input]
+    it "should put #if/#endif and Haskell code into separate blocks" $ do
+      cppSplitBlocks
+        "haskell code\n#if DEBUG\ndebug code\n#endif\nmore haskell code\n" `shouldBe`
+        [ HaskellSource 0 "haskell code"
+        , CPPDirectives "#if DEBUG"
+        , HaskellSource 2 "debug code"
+        , CPPDirectives "#endif"
+        , HaskellSource 4 "more haskell code\n"
+        ]
+    it "should put the shebang line into its own block" $ do
+      cppSplitBlocks
+        "#!/usr/bin/env runhaskell\n{-# LANGUAGE OverloadedStrings #-}\n" `shouldBe`
+        [ Shebang "#!/usr/bin/env runhaskell"
+        , HaskellSource 1 "{-# LANGUAGE OverloadedStrings #-}\n"
+        ]
+    it "should put a multi-line #define into its own block" $ do
+      let input = "#define A \\\n  macro contents \\\n  go here\nhaskell code\n"
+      cppSplitBlocks input `shouldBe`
+        [ CPPDirectives "#define A \\\n  macro contents \\\n  go here"
+        , HaskellSource 3 "haskell code\n"
+        ]
+    it "should put an unterminated multi-line #define into its own block" $ do
+      cppSplitBlocks "#define A \\" `shouldBe` [CPPDirectives "#define A \\"]
+      cppSplitBlocks "#define A \\\n" `shouldBe` [CPPDirectives "#define A \\"]
+      cppSplitBlocks "#define A \\\n.\\" `shouldBe`
+        [CPPDirectives "#define A \\\n.\\"]
+
+markdoneSpec :: Spec
+markdoneSpec = do
+  describe "markdown tokenizer" $ do
+    it "should tokenize plain text" $ do
+      let input =
+            "this is a line\nthis is another line\n\nthis is a new paragraph\n"
+      tokenize input `shouldBe`
+        [ PlainLine "this is a line"
+        , PlainLine "this is another line"
+        , PlainLine ""
+        , PlainLine "this is a new paragraph"
+        ]
+    it "should tokenize headings" $ do
+      tokenize "# Heading" `shouldBe` [Heading 1 "Heading"]
+    it "should tokenize code fence beginnings with labels" $ do
+      tokenize "``` haskell\n" `shouldBe` [BeginFence "haskell"]
+      tokenize "```haskell expect\n" `shouldBe` [BeginFence "haskell expect"]
+      tokenize "before\n```code\nafter\n" `shouldBe`
+        [PlainLine "before", BeginFence "code", PlainLine "after"]
+    it "should tokenize full code fences" $ do
+      tokenize "```haskell\ncode goes here\n```" `shouldBe`
+        [BeginFence "haskell", PlainLine "code goes here", EndFence]
+    it "should tokenize lines inside code fences as plain text" $ do
+      tokenize "```haskell\n#!/usr/bin/env stack\n```" `shouldBe`
+        [BeginFence "haskell", PlainLine "#!/usr/bin/env stack", EndFence]
+      tokenize "```haskell\n# not a heading\n```" `shouldBe`
+        [BeginFence "haskell", PlainLine "# not a heading", EndFence]
+  describe "markdown parser" $ do
+    it "should parse a heading followed by text as a section" $ do
+      let input =
+            [ Heading 1 "This is a heading"
+            , PlainLine "This is plain text"
+            , PlainLine "split across two lines."
+            ]
+      output <- parse input
+      output `shouldBe`
+        [ Section
+            "This is a heading"
+            [PlainText "This is plain text\nsplit across two lines."]
+        ]
