diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Simon Hengel <sol@typeful.net>
+Copyright (c) 2012-2013 Simon Hengel <sol@typeful.net>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/markdown-unlit.cabal b/markdown-unlit.cabal
--- a/markdown-unlit.cabal
+++ b/markdown-unlit.cabal
@@ -1,14 +1,20 @@
+-- This file has been generated from package.yaml by hpack version 0.8.0.
+--
+-- see: https://github.com/sol/hpack
+
 name:             markdown-unlit
-version:          0.2.0.1
+version:          0.3.0
 synopsis:         Literate Haskell support for Markdown
 category:         Development
+homepage:         https://github.com/sol/markdown-unlit#readme
+bug-reports:      https://github.com/sol/markdown-unlit/issues
 license:          MIT
 license-file:     LICENSE
-copyright:        (c) 2012 Simon Hengel
+copyright:        (c) 2012-2013 Simon Hengel
 author:           Simon Hengel <sol@typeful.net>
 maintainer:       Simon Hengel <sol@typeful.net>
 build-type:       Simple
-cabal-version:    >= 1.8
+cabal-version:    >= 1.10
 description:      Documentation is here: <https://github.com/sol/markdown-unlit#readme>
 
 source-repository head
@@ -16,41 +22,45 @@
   location: https://github.com/sol/markdown-unlit
 
 library
-  ghc-options:
-      -Wall
   hs-source-dirs:
       src
+  ghc-options: -Wall
+  build-depends:
+      base == 4.*
+    , base-compat
   exposed-modules:
       Text.Markdown.Unlit
-  build-depends:
-      base < 5
+  default-language: Haskell2010
 
 executable markdown-unlit
-  ghc-options:
-      -Wall
+  main-is: Main.hs
   hs-source-dirs:
       driver
-  main-is:
-      Main.hs
+  ghc-options: -Wall
   build-depends:
-      base
+      base == 4.*
+    , base-compat
     , markdown-unlit
+  default-language: Haskell2010
 
 test-suite spec
-  type:
-      exitcode-stdio-1.0
-  ghc-options:
-      -Wall -Werror
-  cpp-options:
-      -DTEST
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
   hs-source-dirs:
-      src, test
-  main-is:
-      Spec.hs
+      src
+    , test
+  ghc-options: -Wall
+  cpp-options: -DTEST
   build-depends:
-      base
-    , stringbuilder
+      base == 4.*
+    , base-compat
     , directory
-    , silently
-    , hspec >= 1.3
+    , hspec == 2.*
     , QuickCheck
+    , silently
+    , stringbuilder
+    , temporary
+  other-modules:
+      Text.Markdown.Unlit
+      Text.Markdown.UnlitSpec
+  default-language: Haskell2010
diff --git a/src/Text/Markdown/Unlit.hs b/src/Text/Markdown/Unlit.hs
--- a/src/Text/Markdown/Unlit.hs
+++ b/src/Text/Markdown/Unlit.hs
@@ -11,7 +11,8 @@
 #endif
 ) where
 
-import           Control.Applicative
+import           Prelude ()
+import           Prelude.Compat
 import           Data.Maybe
 import           Data.List
 import           Data.Char
@@ -108,14 +109,16 @@
         (cb, rest) -> cb : go rest
 
     takeCB :: Line -> [Line] -> (CodeBlock, [Line])
-    takeCB (n, fence) xs = case break isFence xs of
-      (cb, rest) -> (CodeBlock (parseClasses fence) (map snd cb) n, drop 1 rest)
+    takeCB (n, fence) xs =
+      let indent = length . takeWhile isSpace $ fence
+      in case break isFence xs of
+        (cb, rest) -> (CodeBlock (parseClasses fence) (map (drop indent . snd) cb) n, drop 1 rest)
 
     isFence :: Line -> Bool
-    isFence = isPrefixOf "~~~" . snd
+    isFence = isPrefixOf "~~~" . dropWhile isSpace . snd
 
 parseClasses :: String -> [String]
-parseClasses xs = case dropWhile isSpace . dropWhile (== '~') $ xs of
+parseClasses xs = case dropWhile isSpace . dropWhile (== '~') . dropWhile isSpace $ xs of
   '{':ys -> words . replace '.' ' ' . takeWhile (/= '}') $ ys
   _      -> []
 
diff --git a/test/Text/Markdown/UnlitSpec.hs b/test/Text/Markdown/UnlitSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Text/Markdown/UnlitSpec.hs
@@ -0,0 +1,239 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Text.Markdown.UnlitSpec (main, spec) where
+
+import           Test.Hspec
+import           Test.QuickCheck
+import           Data.String.Builder
+import           System.Environment
+import           Control.Exception
+import           System.Exit
+import           System.IO.Silently
+import           System.IO
+import           System.IO.Temp (withSystemTempFile)
+import           System.Directory
+import qualified Control.Exception as E
+
+import           Text.Markdown.Unlit
+
+main :: IO ()
+main = hspec spec
+
+withTempFile :: (FilePath -> IO ()) -> IO ()
+withTempFile action = withSystemTempFile "hspec" $ \f h -> do
+  hClose h
+  action f `E.finally` removeFile f
+
+spec :: Spec
+spec = do
+  describe "run" $ do
+    it "prints a usage message" $ do
+      withProgName "foo" $ do
+        (r, Left (ExitFailure 1)) <- hCapture [stderr] (try $ run [])
+        r `shouldBe` "usage: foo [selector] -h label infile outfile\n"
+
+    it "unlits code marked with .haskell by default (unless it is marked with .ignore as well)" $ do
+      withTempFile $ \infile -> withTempFile $ \outfile -> do
+        writeFile infile . build $ do
+          "~~~ {.haskell}"
+          "some code"
+
+          "~~~"
+          "~~~ {.haskell .ignore}"
+          "some other code"
+
+          "~~~"
+        run ["-h", "Foo.lhs", infile, outfile]
+        readFile outfile `shouldReturn` (build $ do
+          "#line 2 \"Foo.lhs\""
+          "some code"
+          )
+
+    it "can be customized" $ do
+      withTempFile $ \infile -> withTempFile $ \outfile -> do
+        writeFile infile . build $ do
+          "~~~ {.foo}"
+          "some code"
+          ""
+          "~~~"
+          "~~~ {.bar}"
+          "some other code"
+          "~~~"
+        run ["bar", "-h", "Foo.lhs", infile, outfile]
+        readFile outfile `shouldReturn` (build $ do
+          "#line 6 \"Foo.lhs\""
+          "some other code"
+          )
+
+  describe "parseSelector" $ do
+    it "parses + as :&:" $ do
+      parseSelector "foo+bar+baz" `shouldBe` Just ("foo" :&: "bar" :&: "baz")
+
+    it "parses whitespace as :|:" $ do
+      parseSelector "foo bar baz" `shouldBe` Just ("foo" :|: "bar" :|: "baz")
+
+    it "parses ! as Not" $ do
+      parseSelector "foo+!bar+baz" `shouldBe` Just ("foo" :&: Not "bar" :&: "baz")
+
+    it "can handle a combination of :&: and :|:" $ do
+      parseSelector "foo+bar baz+bar" `shouldBe` Just ("foo" :&: "bar" :|: "baz" :&: "bar")
+
+    it "is total" $ do
+      property $ \xs -> parseSelector xs `seq` True
+
+  describe "unlit" $ do
+    it "can be used to unlit everything with a specified class" $ do
+      unlit "Foo.lhs" "foo" . build $ do
+        "~~~ {.foo}"
+        "foo"
+        "~~~"
+        "~~~ {.bar}"
+        "bar"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 2 \"Foo.lhs\""
+        "foo"
+        )
+
+    it "can handle Not" $ do
+      unlit "Foo.lhs" (Not "foo") . build $ do
+        "~~~ {.foo}"
+        "1"
+        "~~~"
+        "~~~ {.bar}"
+        "2"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 5 \"Foo.lhs\""
+        "2"
+        )
+
+    it "can handle :&:" $ do
+      unlit "Foo.lhs" ("foo" :&: "bar") . build $ do
+        "~~~ {.foo}"
+        "some code"
+        "~~~"
+        "~~~ {.foo .bar}"
+        "some other code"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 5 \"Foo.lhs\""
+        "some other code"
+        )
+
+    it "can handle :|:" $ do
+      unlit "Foo.lhs" ("foo" :|: "bar") . build $ do
+        "~~~ {.foo}"
+        "foo"
+        "~~~"
+        "~~~ {.bar}"
+        "bar"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 2 \"Foo.lhs\""
+        "foo"
+        "#line 5 \"Foo.lhs\""
+        "bar"
+        )
+
+    it "can handle a combination of :&: and :|:" $ do
+      unlit "Foo.lhs" ("foo" :&: "bar" :|: "foo" :&: "baz") . build $ do
+        "~~~ {.foo .bar}"
+        "one"
+        "~~~"
+        "~~~ {.foo .baz}"
+        "two"
+        "~~~"
+        "~~~ {.bar .baz}"
+        "two"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 2 \"Foo.lhs\""
+        "one"
+        "#line 5 \"Foo.lhs\""
+        "two"
+        )
+
+    it "can handle a combination of :&: and Not" $ do
+      unlit "Foo.lhs" ("foo" :&: Not "bar" :&: "baz") . build $ do
+        "~~~ {.foo}"
+        "1"
+        "~~~"
+        "~~~ {.foo .bar}"
+        "2"
+        "~~~"
+        "~~~ {.foo .baz}"
+        "3"
+        "~~~"
+        "~~~ {.foo .bar .baz}"
+        "4"
+        "~~~"
+      `shouldBe` (build $ do
+        "#line 8 \"Foo.lhs\""
+        "3"
+        )
+
+  describe "parse" $ do
+    it "yields an empty list on empty input" $ do
+      parse "" `shouldBe` []
+
+    it "parses a code block" $ do
+      map codeBlockContent . parse . build $ do
+        "some text"
+        "~~~"
+        "some"
+        "code"
+        "~~~"
+        "some other text"
+      `shouldBe` [["some", "code"]]
+
+    it "parses an indented code block" $ do
+      map codeBlockContent . parse . build $ do
+        "1. some text"
+        "    ~~~"
+        "    some"
+        "      code"
+        "    ~~~"
+        "2. some other text"
+      `shouldBe` [["some", "  code"]]
+
+    it "parses an empty code block" $ do
+      map codeBlockContent . parse . build $ do
+        "some text"
+        "~~~"
+        "~~~"
+        "some other text"
+      `shouldBe` [[]]
+
+    it "attaches classes to code blocks" $ do
+      map codeBlockClasses . parse . build $ do
+        "~~~ {.haskell .literate}"
+        "some code"
+        "~~~"
+      `shouldBe` [["haskell", "literate"]]
+
+    it "attaches classes to indented code blocks" $ do
+      map codeBlockClasses . parse . build $ do
+        "1. some text"
+        "    ~~~ {.haskell .literate}"
+        "    some code"
+        "    ~~~"
+        "2. some other text"
+      `shouldBe` [["haskell", "literate"]]
+
+    it "attaches source locations to code blocks" $ do
+      map codeBlockStartLine . parse . build $ do
+        "some text"
+        ""
+        "~~~"
+        "some"
+        "code"
+        "~~~"
+        "some other text"
+      `shouldBe` [4]
+
+  describe "parseClasses" $ do
+    it "drops a leading dot" $ do
+      parseClasses "~~~ {.foo .bar}" `shouldBe` ["foo", "bar"]
+
+    it "treats dots as whitespace" $ do
+      parseClasses "~~~ {foo.bar. ..}" `shouldBe` ["foo", "bar"]
