diff --git a/interpolate.cabal b/interpolate.cabal
--- a/interpolate.cabal
+++ b/interpolate.cabal
@@ -1,5 +1,5 @@
 name:             interpolate
-version:          0.0.4
+version:          0.1.0
 license:          MIT
 license-file:     LICENSE
 copyright:        (c) 2013 Simon Hengel
@@ -24,8 +24,9 @@
   exposed-modules:
       Data.String.Interpolate
       Data.String.Interpolate.IsString
-  other-modules:
       Data.String.Interpolate.Util
+  other-modules:
+      Data.String.Interpolate.Internal.Util
       Data.String.Interpolate.Parse
       Data.String.Interpolate.Compat
   build-depends:
@@ -52,16 +53,3 @@
     , hspec >= 1.5
     , QuickCheck
     , quickcheck-instances
-
-test-suite doctests
-  type:
-      exitcode-stdio-1.0
-  ghc-options:
-      -Wall
-  hs-source-dirs:
-      test
-  main-is:
-      doctests.hs
-  build-depends:
-      base    == 4.*
-    , doctest == 0.9.*
diff --git a/src/Data/String/Interpolate.hs b/src/Data/String/Interpolate.hs
--- a/src/Data/String/Interpolate.hs
+++ b/src/Data/String/Interpolate.hs
@@ -13,7 +13,7 @@
 import           Language.Haskell.TH.Quote (QuasiQuoter(..))
 import           Language.Haskell.Meta.Parse.Careful (parseExp)
 
-import           Data.String.Interpolate.Util
+import           Data.String.Interpolate.Internal.Util
 import           Data.String.Interpolate.Parse
 import           Data.String.Interpolate.Compat (Q, Exp, appE, reportError)
 
diff --git a/src/Data/String/Interpolate/Internal/Util.hs b/src/Data/String/Interpolate/Internal/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/String/Interpolate/Internal/Util.hs
@@ -0,0 +1,118 @@
+module Data.String.Interpolate.Internal.Util where
+
+import           Data.Char
+import           Data.Maybe
+import qualified Numeric as N
+
+import           Data.String.Interpolate.Compat
+
+toString :: Show a => a -> String
+toString a = let s = show a in fromMaybe s (readMaybe s)
+{-# NOINLINE toString #-}
+{-# RULES "toString/String" toString = id #-}
+{-# RULES "toString/Int" toString = show :: Int -> String #-}
+{-# RULES "toString/Integer" toString = show :: Integer -> String #-}
+{-# RULES "toString/Float" toString = show :: Float -> String #-}
+{-# RULES "toString/Double" toString = show :: Double -> String #-}
+
+-- Haskell 2010 character unescaping, see:
+-- http://www.haskell.org/onlinereport/haskell2010/haskellch2.html#x7-200002.6
+unescape :: String -> String
+unescape = go
+  where
+    go input = case input of
+      "" -> ""
+      '\\' : 'x' : x : xs | isHexDigit x -> case span isHexDigit xs of
+        (ys, zs) -> (chr . readHex $ x:ys) : go zs
+      '\\' : 'o' : x : xs | isOctDigit x -> case span isOctDigit xs of
+        (ys, zs) -> (chr . readOct $ x:ys) : go zs
+      '\\' : x : xs | isDigit x -> case span isDigit xs of
+        (ys, zs) -> (chr . read $ x:ys) : go zs
+      '\\' : input_ -> case input_ of
+        '\\' : xs -> '\\' : go xs
+        'a' : xs -> '\a' : go xs
+        'b' : xs -> '\b' : go xs
+        'f' : xs -> '\f' : go xs
+        'n' : xs -> '\n' : go xs
+        'r' : xs -> '\r' : go xs
+        't' : xs -> '\t' : go xs
+        'v' : xs -> '\v' : go xs
+        '&' : xs -> go xs
+        'N':'U':'L' : xs -> '\NUL' : go xs
+        'S':'O':'H' : xs -> '\SOH' : go xs
+        'S':'T':'X' : xs -> '\STX' : go xs
+        'E':'T':'X' : xs -> '\ETX' : go xs
+        'E':'O':'T' : xs -> '\EOT' : go xs
+        'E':'N':'Q' : xs -> '\ENQ' : go xs
+        'A':'C':'K' : xs -> '\ACK' : go xs
+        'B':'E':'L' : xs -> '\BEL' : go xs
+        'B':'S' : xs -> '\BS' : go xs
+        'H':'T' : xs -> '\HT' : go xs
+        'L':'F' : xs -> '\LF' : go xs
+        'V':'T' : xs -> '\VT' : go xs
+        'F':'F' : xs -> '\FF' : go xs
+        'C':'R' : xs -> '\CR' : go xs
+        'S':'O' : xs -> '\SO' : go xs
+        'S':'I' : xs -> '\SI' : go xs
+        'D':'L':'E' : xs -> '\DLE' : go xs
+        'D':'C':'1' : xs -> '\DC1' : go xs
+        'D':'C':'2' : xs -> '\DC2' : go xs
+        'D':'C':'3' : xs -> '\DC3' : go xs
+        'D':'C':'4' : xs -> '\DC4' : go xs
+        'N':'A':'K' : xs -> '\NAK' : go xs
+        'S':'Y':'N' : xs -> '\SYN' : go xs
+        'E':'T':'B' : xs -> '\ETB' : go xs
+        'C':'A':'N' : xs -> '\CAN' : go xs
+        'E':'M' : xs -> '\EM' : go xs
+        'S':'U':'B' : xs -> '\SUB' : go xs
+        'E':'S':'C' : xs -> '\ESC' : go xs
+        'F':'S' : xs -> '\FS' : go xs
+        'G':'S' : xs -> '\GS' : go xs
+        'R':'S' : xs -> '\RS' : go xs
+        'U':'S' : xs -> '\US' : go xs
+        'S':'P' : xs -> '\SP' : go xs
+        'D':'E':'L' : xs -> '\DEL' : go xs
+        '^':'@' : xs -> '\^@' : go xs
+        '^':'A' : xs -> '\^A' : go xs
+        '^':'B' : xs -> '\^B' : go xs
+        '^':'C' : xs -> '\^C' : go xs
+        '^':'D' : xs -> '\^D' : go xs
+        '^':'E' : xs -> '\^E' : go xs
+        '^':'F' : xs -> '\^F' : go xs
+        '^':'G' : xs -> '\^G' : go xs
+        '^':'H' : xs -> '\^H' : go xs
+        '^':'I' : xs -> '\^I' : go xs
+        '^':'J' : xs -> '\^J' : go xs
+        '^':'K' : xs -> '\^K' : go xs
+        '^':'L' : xs -> '\^L' : go xs
+        '^':'M' : xs -> '\^M' : go xs
+        '^':'N' : xs -> '\^N' : go xs
+        '^':'O' : xs -> '\^O' : go xs
+        '^':'P' : xs -> '\^P' : go xs
+        '^':'Q' : xs -> '\^Q' : go xs
+        '^':'R' : xs -> '\^R' : go xs
+        '^':'S' : xs -> '\^S' : go xs
+        '^':'T' : xs -> '\^T' : go xs
+        '^':'U' : xs -> '\^U' : go xs
+        '^':'V' : xs -> '\^V' : go xs
+        '^':'W' : xs -> '\^W' : go xs
+        '^':'X' : xs -> '\^X' : go xs
+        '^':'Y' : xs -> '\^Y' : go xs
+        '^':'Z' : xs -> '\^Z' : go xs
+        '^':'[' : xs -> '\^[' : go xs
+        '^':'\\' : xs -> '\^\' : go xs
+        '^':']' : xs -> '\^]' : go xs
+        '^':'^' : xs -> '\^^' : go xs
+        '^':'_' : xs -> '\^_' : go xs
+        xs -> go xs
+      x:xs -> x : go xs
+
+    readHex :: String -> Int
+    readHex xs = case N.readHex xs of
+      [(n, "")] -> n
+      _ -> error "Data.String.Interpolate.Util.readHex: no parse"
+
+    readOct :: String -> Int
+    readOct xs = case N.readOct xs of
+      [(n, "")] -> n
+      _ -> error "Data.String.Interpolate.Util.readHex: no parse"
diff --git a/src/Data/String/Interpolate/Parse.hs b/src/Data/String/Interpolate/Parse.hs
--- a/src/Data/String/Interpolate/Parse.hs
+++ b/src/Data/String/Interpolate/Parse.hs
@@ -1,6 +1,6 @@
 module Data.String.Interpolate.Parse where
 
-import           Data.String.Interpolate.Util
+import           Data.String.Interpolate.Internal.Util
 
 data Node = Literal String | Expression String
 
diff --git a/src/Data/String/Interpolate/Util.hs b/src/Data/String/Interpolate/Util.hs
--- a/src/Data/String/Interpolate/Util.hs
+++ b/src/Data/String/Interpolate/Util.hs
@@ -1,118 +1,79 @@
-module Data.String.Interpolate.Util where
+module Data.String.Interpolate.Util (unindent) where
 
+import           Control.Arrow ((>>>))
 import           Data.Char
-import           Data.Maybe
-import qualified Numeric as N
 
-import           Data.String.Interpolate.Compat
+-- | Remove indentation as much as possible while preserving relative
+-- indentation levels.
+--
+-- `unindent` is useful in combination with `Data.String.Interpolate.i` to remove leading spaces that
+-- resulted from code indentation.  That way you can freely indent your string
+-- literals without the indentation ending up in the resulting strings.
+--
+-- Here is an example:
+--
+-- >>> :set -XQuasiQuotes
+-- >>> import Data.String.Interpolate
+-- >>> import Data.String.Interpolate.Util
+-- >>> :{
+--  putStr $ unindent [i|
+--      def foo
+--        23
+--      end
+--    |]
+-- :}
+-- def foo
+--   23
+-- end
+--
+-- To allow this, two additional things are being done, apart from removing
+-- indentation:
+--
+-- - One empty line at the beginning will be removed and
+-- - if the last newline character (@"\\n"@) is followed by spaces, the spaces are removed.
+unindent :: String -> String
+unindent =
+      lines_
+  >>> removeLeadingEmptyLine
+  >>> trimLastLine
+  >>> removeIndentation
+  >>> concat
+  where
+    isEmptyLine :: String -> Bool
+    isEmptyLine = all isSpace
 
-toString :: Show a => a -> String
-toString a = let s = show a in fromMaybe s (readMaybe s)
-{-# NOINLINE toString #-}
-{-# RULES "toString/String" toString = id #-}
-{-# RULES "toString/Int" toString = show :: Int -> String #-}
-{-# RULES "toString/Integer" toString = show :: Integer -> String #-}
-{-# RULES "toString/Float" toString = show :: Float -> String #-}
-{-# RULES "toString/Double" toString = show :: Double -> String #-}
+    lines_ :: String -> [String]
+    lines_ [] = []
+    lines_ s = case span (/= '\n') s of
+      (first, '\n' : rest) -> (first ++ "\n") : lines_ rest
+      (first, rest) -> first : lines_ rest
 
--- Haskell 2010 character unescaping, see:
--- http://www.haskell.org/onlinereport/haskell2010/haskellch2.html#x7-200002.6
-unescape :: String -> String
-unescape = go
-  where
-    go input = case input of
-      "" -> ""
-      '\\' : 'x' : x : xs | isHexDigit x -> case span isHexDigit xs of
-        (ys, zs) -> (chr . readHex $ x:ys) : go zs
-      '\\' : 'o' : x : xs | isOctDigit x -> case span isOctDigit xs of
-        (ys, zs) -> (chr . readOct $ x:ys) : go zs
-      '\\' : x : xs | isDigit x -> case span isDigit xs of
-        (ys, zs) -> (chr . read $ x:ys) : go zs
-      '\\' : input_ -> case input_ of
-        '\\' : xs -> '\\' : go xs
-        'a' : xs -> '\a' : go xs
-        'b' : xs -> '\b' : go xs
-        'f' : xs -> '\f' : go xs
-        'n' : xs -> '\n' : go xs
-        'r' : xs -> '\r' : go xs
-        't' : xs -> '\t' : go xs
-        'v' : xs -> '\v' : go xs
-        '&' : xs -> go xs
-        'N':'U':'L' : xs -> '\NUL' : go xs
-        'S':'O':'H' : xs -> '\SOH' : go xs
-        'S':'T':'X' : xs -> '\STX' : go xs
-        'E':'T':'X' : xs -> '\ETX' : go xs
-        'E':'O':'T' : xs -> '\EOT' : go xs
-        'E':'N':'Q' : xs -> '\ENQ' : go xs
-        'A':'C':'K' : xs -> '\ACK' : go xs
-        'B':'E':'L' : xs -> '\BEL' : go xs
-        'B':'S' : xs -> '\BS' : go xs
-        'H':'T' : xs -> '\HT' : go xs
-        'L':'F' : xs -> '\LF' : go xs
-        'V':'T' : xs -> '\VT' : go xs
-        'F':'F' : xs -> '\FF' : go xs
-        'C':'R' : xs -> '\CR' : go xs
-        'S':'O' : xs -> '\SO' : go xs
-        'S':'I' : xs -> '\SI' : go xs
-        'D':'L':'E' : xs -> '\DLE' : go xs
-        'D':'C':'1' : xs -> '\DC1' : go xs
-        'D':'C':'2' : xs -> '\DC2' : go xs
-        'D':'C':'3' : xs -> '\DC3' : go xs
-        'D':'C':'4' : xs -> '\DC4' : go xs
-        'N':'A':'K' : xs -> '\NAK' : go xs
-        'S':'Y':'N' : xs -> '\SYN' : go xs
-        'E':'T':'B' : xs -> '\ETB' : go xs
-        'C':'A':'N' : xs -> '\CAN' : go xs
-        'E':'M' : xs -> '\EM' : go xs
-        'S':'U':'B' : xs -> '\SUB' : go xs
-        'E':'S':'C' : xs -> '\ESC' : go xs
-        'F':'S' : xs -> '\FS' : go xs
-        'G':'S' : xs -> '\GS' : go xs
-        'R':'S' : xs -> '\RS' : go xs
-        'U':'S' : xs -> '\US' : go xs
-        'S':'P' : xs -> '\SP' : go xs
-        'D':'E':'L' : xs -> '\DEL' : go xs
-        '^':'@' : xs -> '\^@' : go xs
-        '^':'A' : xs -> '\^A' : go xs
-        '^':'B' : xs -> '\^B' : go xs
-        '^':'C' : xs -> '\^C' : go xs
-        '^':'D' : xs -> '\^D' : go xs
-        '^':'E' : xs -> '\^E' : go xs
-        '^':'F' : xs -> '\^F' : go xs
-        '^':'G' : xs -> '\^G' : go xs
-        '^':'H' : xs -> '\^H' : go xs
-        '^':'I' : xs -> '\^I' : go xs
-        '^':'J' : xs -> '\^J' : go xs
-        '^':'K' : xs -> '\^K' : go xs
-        '^':'L' : xs -> '\^L' : go xs
-        '^':'M' : xs -> '\^M' : go xs
-        '^':'N' : xs -> '\^N' : go xs
-        '^':'O' : xs -> '\^O' : go xs
-        '^':'P' : xs -> '\^P' : go xs
-        '^':'Q' : xs -> '\^Q' : go xs
-        '^':'R' : xs -> '\^R' : go xs
-        '^':'S' : xs -> '\^S' : go xs
-        '^':'T' : xs -> '\^T' : go xs
-        '^':'U' : xs -> '\^U' : go xs
-        '^':'V' : xs -> '\^V' : go xs
-        '^':'W' : xs -> '\^W' : go xs
-        '^':'X' : xs -> '\^X' : go xs
-        '^':'Y' : xs -> '\^Y' : go xs
-        '^':'Z' : xs -> '\^Z' : go xs
-        '^':'[' : xs -> '\^[' : go xs
-        '^':'\\' : xs -> '\^\' : go xs
-        '^':']' : xs -> '\^]' : go xs
-        '^':'^' : xs -> '\^^' : go xs
-        '^':'_' : xs -> '\^_' : go xs
-        xs -> go xs
-      x:xs -> x : go xs
+    removeLeadingEmptyLine :: [String] -> [String]
+    removeLeadingEmptyLine xs = case xs of
+      y:ys | isEmptyLine y -> ys
+      _ -> xs
 
-    readHex :: String -> Int
-    readHex xs = case N.readHex xs of
-      [(n, "")] -> n
-      _ -> error "Data.String.Interpolate.Util.readHex: no parse"
+    trimLastLine :: [String] -> [String]
+    trimLastLine (a : b : r) = a : trimLastLine (b : r)
+    trimLastLine [a] = if all (== ' ') a
+      then []
+      else [a]
+    trimLastLine [] = []
 
-    readOct :: String -> Int
-    readOct xs = case N.readOct xs of
-      [(n, "")] -> n
-      _ -> error "Data.String.Interpolate.Util.readHex: no parse"
+    removeIndentation :: [String] -> [String]
+    removeIndentation ys = map (dropSpaces indentation) ys
+      where
+        dropSpaces 0 s = s
+        dropSpaces n (' ' : r) = dropSpaces (n - 1) r
+        dropSpaces _ s = s
+        indentation = minimalIndentation ys
+        minimalIndentation =
+            safeMinimum 0
+          . map (length . takeWhile (== ' '))
+          . removeEmptyLines
+        removeEmptyLines = filter (not . isEmptyLine)
+
+        safeMinimum :: Ord a => a -> [a] -> a
+        safeMinimum x xs = case xs of
+          [] -> x
+          _ -> minimum xs
diff --git a/test/doctests.hs b/test/doctests.hs
deleted file mode 100644
--- a/test/doctests.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-module Main where
-
-import           Test.DocTest
-
-main :: IO ()
-main = doctest ["-isrc", "-optP-include", "-optPdist/build/autogen/cabal_macros.h", "src/Data/String/Interpolate.hs"]
