directory-layout-0.7.1.0: src/System/Directory/Layout/QQ.hs
{-# LANGUAGE TemplateHaskell #-}
-- | Convenience quasiquoter to ease the pain working with multiline strings
module System.Directory.Layout.QQ (dedent) where
import Control.Applicative
import Data.Char (isSpace)
import Data.Foldable (Foldable(..), toList)
import Data.List (intercalate)
import Data.Sequence (Seq, ViewL(..), ViewR(..), (|>))
import qualified Data.Sequence as Seq
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Language.Haskell.TH.Syntax (liftString)
import Language.Haskell.TH (Q, Exp)
import Prelude hiding (foldr)
-- | A handy quasiquoter to work with the multiline file contents
--
-- Strips the longest common leading spaces segment. All spacey characters are treated
-- equally. The first line is ignored if it's spaces only.
--
-- >>> :set -XQuasiQuotes
-- >>> :{
-- putStr [dedent|
-- hello
-- world
-- !
-- |]
-- :}
-- hello
-- world
-- !
dedent :: QuasiQuoter
dedent = quoter $
liftString . withLines (strip . trim (all isSpace))
withLines :: (Seq String -> Seq String) -> String -> String
withLines f = unsplit '\n' . toList . f . Seq.fromList . split '\n'
split :: Eq a => a -> [a] -> [[a]]
split sep xs = case break (== sep) xs of
(ys, []) -> ys : []
(ys, _ : zs) -> ys : split sep zs
unsplit :: a -> [[a]] -> [a]
unsplit = intercalate . pure
-- | The first line can be safely dropped if it consists only of spaces,
-- but we want to preserve the last newline, thus last line can only be trimmed
trim :: (String -> Bool) -> Seq String -> Seq String
trim f = trimLast f . dropFirst f
dropFirst :: (String -> Bool) -> Seq String -> Seq String
dropFirst p xs = case Seq.viewl xs of
y :< ys | p y -> ys
_ -> xs
trimLast :: (String -> Bool) -> Seq String -> Seq String
trimLast p xs = case Seq.viewr xs of
ys :> y | p y -> ys |> ""
_ -> xs
strip :: Seq String -> Seq String
strip xs = case Seq.viewr xs of
vs :> "" -> stripCommonLeadingWhitespace vs |> ""
_ -> stripCommonLeadingWhitespace xs
stripCommonLeadingWhitespace :: (Functor f, Foldable f) => f String -> f String
stripCommonLeadingWhitespace xs = drop (commonLeadingWhitespace xs) <$> xs
commonLeadingWhitespace :: (Functor f, Foldable f) => f String -> Int
commonLeadingWhitespace = minimumOr 0 . fmap (length . takeWhile isSpace)
minimumOr :: (Foldable f, Ord a) => a -> f a -> a
minimumOr n = maybe n id . foldr (lmin . Just) Nothing
where
lmin (Just x) (Just y) = Just (min x y)
lmin Nothing x = x
lmin x Nothing = x
quoter :: (String -> Q Exp) -> QuasiQuoter
quoter quote = QuasiQuoter
{ quoteExp = quote
, quotePat = failure "patterns"
, quoteType = failure "types"
, quoteDec = failure "declarations"
}
where
failure kind =
fail $ "this quasiquoter does not support splicing " ++ kind