neat-interpolation 0.2.0 → 0.2.1
raw patch · 10 files changed
+248/−358 lines, 10 filesdep +HTFdep +base-preludedep +neat-interpolationdep ~basedep ~parsecdep ~template-haskell
Dependencies added: HTF, base-prelude, neat-interpolation
Dependency ranges changed: base, parsec, template-haskell
Files
- executables/APITests.hs +26/−0
- library/NeatInterpolation.hs +123/−0
- library/NeatInterpolation/Parsing.hs +35/−0
- library/NeatInterpolation/String.hs +46/−0
- neat-interpolation.cabal +18/−75
- src/Demo.hs +0/−26
- src/NeatInterpolation.hs +0/−123
- src/NeatInterpolation/Parsing.hs +0/−35
- src/NeatInterpolation/Prelude.hs +0/−52
- src/NeatInterpolation/String.hs +0/−47
+ executables/APITests.hs view
@@ -0,0 +1,26 @@+{-# OPTIONS_GHC -F -pgmF htfpp #-}++import BasePrelude+import Test.Framework+import NeatInterpolation+++main = htfMain $ htf_thisModulesTests+++test_demo =+ assertEqual+ "function(){\n function(){\n {\n indented line\n indented line\n }\n }\n return {\n indented line\n indented line\n }\n}\n"+ (template a a)+ where+ template a b = + [string|+ function(){+ function(){+ $a+ }+ return $b+ }+ |]+ a = "{\n indented line\n indented line\n}"+
+ library/NeatInterpolation.hs view
@@ -0,0 +1,123 @@+{-# OPTIONS_GHC -fno-warn-missing-fields #-} +-- | +-- 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, OverloadedStrings #-}+-- > +-- > import NeatInterpolation+-- > +-- > f :: String -> String -> String+-- > f a b = +-- > [string|+-- > function(){+-- > function(){+-- > $a+-- > }+-- > return $b+-- > }+-- > |]+-- +-- Executing the following:+-- +-- > main = 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 = 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? +module NeatInterpolation (string, indentQQPlaceholder) where++import BasePrelude++import Language.Haskell.TH+import Language.Haskell.TH.Quote++import NeatInterpolation.String+import NeatInterpolation.Parsing+++-- |+-- The quasiquoter.+string :: QuasiQuoter+string = QuasiQuoter {quoteExp = quoteExprExp}++-- |+-- A function used internally by quasiquoter. Just ignore it.+indentQQPlaceholder :: Int -> String -> String+indentQQPlaceholder indent text = case lines text of+ head:tail -> intercalate "\n" $ head : map (replicate indent ' ' ++) tail+ [] -> text +++quoteExprExp :: String -> Q Exp+quoteExprExp input = + case parseLines $ normalizeQQInput input of+ Left e -> fail $ show e+ Right lines -> appE [|unlines|] $ linesExp lines++linesExp :: [Line] -> Q Exp+linesExp [] = [|([] :: [String])|]+linesExp (head : tail) = + (binaryOpE [|(:)|])+ (lineExp head)+ (linesExp tail)++lineExp :: Line -> Q Exp+lineExp (Line indent contents) = + msumExps $ map (contentExp $ fromIntegral indent) contents++++contentExp :: Integer -> LineContent -> Q Exp+contentExp _ (LineContentText text) = stringE text+contentExp indent (LineContentIdentifier name) = do+ valueName <- lookupValueName name+ case valueName of+ Just valueName -> do+ Just indentQQPlaceholderName <- lookupValueName "indentQQPlaceholder"+ appE+ (appE (varE indentQQPlaceholderName) $ litE $ integerL indent)+ (varE valueName)+ Nothing -> fail $ "Value `" ++ name ++ "` is not in scope"++msumExps :: [Q Exp] -> Q Exp+msumExps = foldr (binaryOpE mappendE) memptyE+memptyE = [|mempty|]+mappendE = [|mappend|]++binaryOpE e = \a b -> e `appE` a `appE` b
+ library/NeatInterpolation/Parsing.hs view
@@ -0,0 +1,35 @@+module NeatInterpolation.Parsing where++import BasePrelude hiding (try, (<|>), many)+import Text.Parsec hiding (Line)+++data Line = + Line {lineIndent :: Int, lineContents :: [LineContent]}+ deriving (Show)++data LineContent = + LineContentText [Char] |+ LineContentIdentifier [Char]+ deriving (Show)+++parseLines :: [Char] -> Either ParseError [Line]+parseLines = parse lines "NeatInterpolation.Parsing.parseLines"+ where+ lines = sepBy line newline <* eof+ line = Line <$> countIndent <*> many content+ countIndent = fmap length $ try $ lookAhead $ many $ char ' '+ content = try identifier <|> contentText+ identifier = fmap LineContentIdentifier $ + string "$" *> many1 (alphaNum <|> char '\'' <|> char '_')+ contentText = do+ text <- manyTill anyChar end+ if null text+ then fail "Empty text"+ else return $ LineContentText $ text+ where+ end = + (void $ try $ lookAhead identifier) <|> + (void $ try $ lookAhead newline) <|> + eof
+ library/NeatInterpolation/String.hs view
@@ -0,0 +1,46 @@+module NeatInterpolation.String where++import BasePrelude+++normalizeQQInput :: [Char] -> [Char]+normalizeQQInput = trim . unindent' . tabsToSpaces+ where+ unindent' :: [Char] -> [Char]+ unindent' s =+ case lines s of+ head:tail -> + let + unindentedHead = dropWhile (== ' ') head + minimumTailIndent = minimumIndent . unlines $ tail+ unindentedTail = case minimumTailIndent of+ Just indent -> map (drop indent) tail+ Nothing -> tail+ in unlines $ unindentedHead : unindentedTail+ [] -> []++trim :: [Char] -> [Char]+trim = dropWhileRev isSpace . dropWhile isSpace++dropWhileRev :: (a -> Bool) -> [a] -> [a]+dropWhileRev p = foldr (\x xs -> if p x && null xs then [] else x:xs) []++unindent :: [Char] -> [Char]+unindent s =+ case minimumIndent s of+ Just indent -> unlines . map (drop indent) . lines $ s+ Nothing -> s++tabsToSpaces :: [Char] -> [Char]+tabsToSpaces ('\t':tail) = " " ++ tabsToSpaces tail+tabsToSpaces (head:tail) = head : tabsToSpaces tail+tabsToSpaces [] = []++minimumIndent :: [Char] -> Maybe Int+minimumIndent = + listToMaybe . sort . map lineIndent + . filter (not . null . dropWhile isSpace) . lines++-- | Amount of preceding spaces on first line+lineIndent :: [Char] -> Int+lineIndent = length . takeWhile (== ' ')
neat-interpolation.cabal view
@@ -1,7 +1,7 @@ name: neat-interpolation version:- 0.2.0+ 0.2.1 synopsis: A quasiquoter for neat and simple multiline text interpolation description:@@ -40,94 +40,37 @@ library hs-source-dirs:- src+ library exposed-modules: NeatInterpolation other-modules:- NeatInterpolation.Prelude NeatInterpolation.Parsing NeatInterpolation.String build-depends:- parsec,- template-haskell,- base >= 4.5 && < 5+ parsec >= 3 && < 3.2,+ template-haskell >= 2.8 && < 2.10,+ base-prelude == 0.1.*,+ base >= 4.5 && < 4.8+ ghc-options:+ -funbox-strict-fields default-extensions:- NoImplicitPrelude- PatternGuards- MultiWayIf- LambdaCase- Arrows- MultiParamTypeClasses- NoMonomorphismRestriction- FlexibleInstances- FlexibleContexts- GADTs- EmptyDataDecls- StandaloneDeriving- ConstraintKinds- RankNTypes- TypeFamilies- TypeOperators- DataKinds- ImpredicativeTypes- LiberalTypeSynonyms- OverloadedStrings- TemplateHaskell- QuasiQuotes- GeneralizedNewtypeDeriving- DeriveDataTypeable- DeriveGeneric- DefaultSignatures- ScopedTypeVariables- BangPatterns- RecordWildCards- TupleSections+ Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFunctor, DeriveGeneric, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, ImpredicativeTypes, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeFamilies, TypeOperators, UnboxedTuples default-language: Haskell2010 -executable neat-interpolation-demo+test-suite api-tests+ type:+ exitcode-stdio-1.0 hs-source-dirs:- src+ executables main-is:- Demo.hs- ghc-options:- -threaded - "-with-rtsopts=-N"+ APITests.hs build-depends:- parsec,- template-haskell,- base >= 4.5 && < 5+ neat-interpolation,+ HTF == 0.11.*,+ base-prelude == 0.1.* default-extensions:- NoImplicitPrelude- PatternGuards- MultiWayIf- LambdaCase- Arrows- MultiParamTypeClasses- NoMonomorphismRestriction- FlexibleInstances- FlexibleContexts- GADTs- EmptyDataDecls- StandaloneDeriving- ConstraintKinds- RankNTypes- TypeFamilies- TypeOperators- DataKinds- ImpredicativeTypes- LiberalTypeSynonyms- OverloadedStrings- TemplateHaskell- QuasiQuotes- GeneralizedNewtypeDeriving- DeriveDataTypeable- DeriveGeneric- DefaultSignatures- ScopedTypeVariables- BangPatterns- RecordWildCards- TupleSections+ Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFunctor, DeriveGeneric, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, ImpredicativeTypes, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeFamilies, TypeOperators, UnboxedTuples default-language: Haskell2010
− src/Demo.hs
@@ -1,26 +0,0 @@--import NeatInterpolation.Prelude-import NeatInterpolation--main = do- let a' = [string| - function startsWith( start, string ){- return string.lastIndexOf( start ) == 0- }- |]- let pattern = [string| {- -- $a'-- single inlining: ( $a' )--- {- multiple inlining: ( $a' ) ( $a' )- }--- } - |]- putStrLn pattern
− src/NeatInterpolation.hs
@@ -1,123 +0,0 @@-{-# OPTIONS_GHC -fno-warn-missing-fields #-} --- | --- 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, OverloadedStrings #-}--- > --- > import NeatInterpolation--- > --- > f :: String -> String -> String--- > f a b = --- > [string|--- > function(){--- > function(){--- > $a--- > }--- > return $b--- > }--- > |]--- --- Executing the following:--- --- > main = 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 = 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? -module NeatInterpolation (string, indentQQPlaceholder) where--import NeatInterpolation.Prelude--import Language.Haskell.TH-import Language.Haskell.TH.Quote--import NeatInterpolation.String-import NeatInterpolation.Parsing----- |--- The quasiquoter.-string :: QuasiQuoter-string = QuasiQuoter {quoteExp = quoteExprExp}---- |--- A function used internally by quasiquoter. Just ignore it.-indentQQPlaceholder :: Int -> String -> String-indentQQPlaceholder indent text = case lines text of- head:tail -> intercalate "\n" $ head : map (replicate indent ' ' ++) tail- [] -> text ---quoteExprExp :: String -> Q Exp-quoteExprExp input = - case parseLines $ normalizeQQInput input of- Left e -> fail $ show e- Right lines -> appE [|unlines|] $ linesExp lines--linesExp :: [Line] -> Q Exp-linesExp [] = [|([] :: [String])|]-linesExp (head : tail) = - (binaryOpE [|(:)|])- (lineExp head)- (linesExp tail)--lineExp :: Line -> Q Exp-lineExp (Line indent contents) = - msumExps $ map (contentExp $ fromIntegral indent) contents----contentExp :: Integer -> LineContent -> Q Exp-contentExp _ (LineContentText text) = stringE text-contentExp indent (LineContentIdentifier name) = do- valueName <- lookupValueName name- case valueName of- Just valueName -> do- Just indentQQPlaceholderName <- lookupValueName "indentQQPlaceholder"- appE- (appE (varE indentQQPlaceholderName) $ litE $ integerL indent)- (varE valueName)- Nothing -> fail $ "Value `" ++ name ++ "` is not in scope"--msumExps :: [Q Exp] -> Q Exp-msumExps = foldr (binaryOpE mappendE) memptyE-memptyE = [|mempty|]-mappendE = [|mappend|]--binaryOpE e = \a b -> e `appE` a `appE` b
− src/NeatInterpolation/Parsing.hs
@@ -1,35 +0,0 @@-module NeatInterpolation.Parsing where--import NeatInterpolation.Prelude hiding (try, (<|>), many)-import Text.Parsec hiding (Line)--- import qualified Data.Attoparsec.Text as AP--data Line = - Line {lineIndent :: Int, lineContents :: [LineContent]}- deriving (Show)--data LineContent = - LineContentText [Char] |- LineContentIdentifier [Char]- deriving (Show)---parseLines :: [Char] -> Either ParseError [Line]-parseLines = parse lines "NeatInterpolation.Parsing.parseLines"- where- lines = sepBy line newline <* eof- line = Line <$> countIndent <*> many content- countIndent = fmap length $ try $ lookAhead $ many $ char ' '- content = try identifier <|> contentText- identifier = fmap LineContentIdentifier $ - string "$" *> many1 (alphaNum <|> char '\'' <|> char '_')- contentText = do- text <- manyTill anyChar end- if null text- then fail "Empty text"- else return $ LineContentText $ text- where- end = - (void $ try $ lookAhead identifier) <|> - (void $ try $ lookAhead newline) <|> - eof
− src/NeatInterpolation/Prelude.hs
@@ -1,52 +0,0 @@-module NeatInterpolation.Prelude - ( - module Exports,- (?:),- traceM,- )- where---- base-import Prelude as Exports hiding (concat, foldr, mapM_, sequence_, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, mapM, sequence, FilePath, id, (.))-import Control.Monad as Exports hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)-import Control.Applicative as Exports-import Control.Arrow as Exports hiding (left, right)-import Control.Category as Exports-import Data.Monoid as Exports-import Data.Foldable as Exports-import Data.Traversable as Exports hiding (for)-import Data.Maybe as Exports-import Data.List as Exports hiding (concat, foldr, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, find, maximumBy, minimumBy, mapAccumL, mapAccumR, foldl')-import Data.Tuple as Exports-import Data.Ord as Exports (Down(..))-import Data.String as Exports-import Data.Int as Exports-import Data.Word as Exports-import Data.Ratio as Exports-import Data.Fixed as Exports-import Data.Ix as Exports-import Data.Data as Exports-import Text.Read as Exports (readMaybe, readEither)-import Control.Exception as Exports hiding (tryJust)-import Control.Concurrent as Exports hiding (yield)-import System.Timeout as Exports-import System.Exit as Exports-import System.IO.Unsafe as Exports-import System.IO as Exports (Handle, hClose)-import System.IO.Error as Exports-import Unsafe.Coerce as Exports-import GHC.Exts as Exports (groupWith, sortWith)-import GHC.Generics as Exports (Generic)-import GHC.IO.Exception as Exports-import Debug.Trace as Exports-import Data.IORef as Exports-import Data.STRef as Exports-import Control.Monad.ST as Exports---(?:) :: Maybe a -> a -> a-maybeA ?: b = fromMaybe b maybeA-{-# INLINE (?:) #-}--traceM :: (Monad m) => String -> m ()-traceM s = trace s $ return ()
− src/NeatInterpolation/String.hs
@@ -1,47 +0,0 @@-module NeatInterpolation.String where--import NeatInterpolation.Prelude-import Data.Char---normalizeQQInput :: [Char] -> [Char]-normalizeQQInput = trim . unindent' . tabsToSpaces- where- unindent' :: [Char] -> [Char]- unindent' s =- case lines s of- head:tail -> - let - unindentedHead = dropWhile (== ' ') head - minimumTailIndent = minimumIndent . unlines $ tail- unindentedTail = case minimumTailIndent of- Just indent -> map (drop indent) tail- Nothing -> tail- in unlines $ unindentedHead : unindentedTail- [] -> []--trim :: [Char] -> [Char]-trim = dropWhileRev isSpace . dropWhile isSpace--dropWhileRev :: (a -> Bool) -> [a] -> [a]-dropWhileRev p = foldr (\x xs -> if p x && null xs then [] else x:xs) []--unindent :: [Char] -> [Char]-unindent s =- case minimumIndent s of- Just indent -> unlines . map (drop indent) . lines $ s- Nothing -> s--tabsToSpaces :: [Char] -> [Char]-tabsToSpaces ('\t':tail) = " " ++ tabsToSpaces tail-tabsToSpaces (head:tail) = head : tabsToSpaces tail-tabsToSpaces [] = []--minimumIndent :: [Char] -> Maybe Int-minimumIndent = - listToMaybe . sort . map lineIndent - . filter (not . null . dropWhile isSpace) . lines---- | Amount of preceding spaces on first line-lineIndent :: [Char] -> Int-lineIndent = length . takeWhile (== ' ')