neat-interpolation-0.5.1.4: library/NeatInterpolation.hs
-- |
-- NeatInterpolation provides a quasiquoter for producing strings
-- with a simple interpolation of input values.
-- It removes the excessive indentation from the input and
-- accurately manages the indentation of all lines of interpolated variables.
-- But enough words, the code shows it better.
--
-- Consider the following declaration:
--
-- > {-# LANGUAGE QuasiQuotes #-}
-- >
-- > import NeatInterpolation
-- > import Data.Text (Text)
-- >
-- > f :: Text -> Text -> Text
-- > f a b =
-- > [trimming|
-- > function(){
-- > function(){
-- > $a
-- > }
-- > return $b
-- > }
-- > |]
--
-- Executing the following:
--
-- > main = Text.putStrLn $ f "1" "2"
--
-- will produce this (notice the reduced indentation compared to how it was
-- declared):
--
-- > function(){
-- > function(){
-- > 1
-- > }
-- > return 2
-- > }
--
-- Now let's test it with multiline string parameters:
--
-- > main = Text.putStrLn $ f
-- > "{\n indented line\n indented line\n}"
-- > "{\n indented line\n indented line\n}"
--
-- We get
--
-- > function(){
-- > function(){
-- > {
-- > indented line
-- > indented line
-- > }
-- > }
-- > return {
-- > indented line
-- > indented line
-- > }
-- > }
--
-- See how it neatly preserved the indentation levels of lines the
-- variable placeholders were at?
--
-- If you need to separate variable placeholder from the following text to
-- prevent treating the rest of line as variable name, use escaped variable:
--
-- > f name = [trimming|this_could_be_${name}_long_identifier|]
--
-- So
--
-- > f "one" == "this_could_be_one_long_identifier"
--
-- If you want to write something that looks like a variable but should be
-- inserted as-is, escape it with another @$@:
--
-- > f word = [trimming|$$my ${word} $${string}|]
--
-- results in
--
-- > f "funny" == "$my funny ${string}"
module NeatInterpolation (trimming, untrimming, text) where
import qualified Data.Text as Text
import Language.Haskell.TH
import Language.Haskell.TH.Quote hiding (quoteExp)
import qualified NeatInterpolation.Parsing as Parsing
import NeatInterpolation.Prelude
import qualified NeatInterpolation.String as String
expQQ :: (String -> Q Exp) -> QuasiQuoter
expQQ quoteExp = QuasiQuoter quoteExp notSupported notSupported notSupported
where
notSupported _ = fail "Quotation in this context is not supported"
-- |
-- An alias to `trimming` for backward-compatibility.
text :: QuasiQuoter
text = trimming
-- |
-- Trimmed quasiquoter variation.
-- Same as `untrimming`, but also
-- removes the leading and trailing whitespace.
trimming :: QuasiQuoter
trimming = expQQ (quoteExp . String.trim . String.unindent . String.tabsToSpaces)
-- |
-- Untrimmed quasiquoter variation.
-- Unindents the quoted template and converts tabs to spaces.
untrimming :: QuasiQuoter
untrimming = expQQ (quoteExp . String.unindent . String.tabsToSpaces)
indentQQPlaceholder :: Int -> Text -> Text
indentQQPlaceholder indent text = case Text.lines text of
head : tail ->
Text.intercalate (Text.singleton '\n') $
head : map (Text.replicate indent (Text.singleton ' ') <>) tail
[] -> text
quoteExp :: String -> Q Exp
quoteExp input =
case Parsing.parseLines input of
Left e -> fail $ show e
Right lines ->
sigE
(appE [|Text.intercalate (Text.singleton '\n')|] $ listE $ map lineExp lines)
[t|Text|]
lineExp :: Parsing.Line -> Q Exp
lineExp (Parsing.Line indent contents) =
case contents of
[] -> [|Text.empty|]
[x] -> toExp x
xs -> appE [|Text.concat|] $ listE $ map toExp xs
where
toExp = contentExp (fromIntegral indent)
contentExp :: Integer -> Parsing.LineContent -> Q Exp
contentExp _ (Parsing.LineContentText text) = appE [|Text.pack|] (stringE text)
contentExp indent (Parsing.LineContentIdentifier name) = do
valueName <- lookupValueName name
case valueName of
Just valueName -> do
appE
(appE (varE 'indentQQPlaceholder) $ litE $ integerL indent)
(varE valueName)
Nothing -> fail $ "Value `" ++ name ++ "` is not in scope"