├── .github └── workflows │ ├── cd.yaml │ └── ci.yaml ├── CHANGELOG.md ├── LICENSE ├── cabal.project ├── library ├── NeatInterpolation.hs └── NeatInterpolation │ ├── Parsing.hs │ ├── Prelude.hs │ └── String.hs ├── neat-interpolation.cabal └── test └── Main.hs /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: Release the lib to Hackage 2 | 3 | on: 4 | push: 5 | branches: 6 | - supermajor 7 | - major 8 | - minor 9 | - patch 10 | 11 | concurrency: 12 | group: cd 13 | cancel-in-progress: false 14 | 15 | jobs: 16 | 17 | ci: 18 | uses: ./.github/workflows/ci.yaml 19 | secrets: inherit 20 | 21 | cd: 22 | needs: 23 | - ci 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Release 29 | uses: nikita-volkov/release-haskell-package.github-action@v1.2.0 30 | with: 31 | hackage-token: ${{ secrets.HACKAGE_TOKEN }} 32 | version-bump-place: ${{ fromJSON('{"supermajor":0,"major":1,"minor":2,"patch":3}')[github.ref_name] }} 33 | main-branch: master 34 | prefix-tag-with-v: true 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Compile, test and check the docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_call: 9 | 10 | jobs: 11 | 12 | format: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: nikita-volkov/cabal-fmt.github-action@v1.0.0 19 | - uses: nikita-volkov/ormolu.github-action@v1.0.0 20 | - name: Commit the changes 21 | uses: stefanzweifel/git-auto-commit-action@v5 22 | with: 23 | commit_message: Format 24 | 25 | build-and-test: 26 | 27 | needs: 28 | - format 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | include: 34 | - ghc: '8.8.4' 35 | - ghc: '9.6.3' 36 | ghc-options: -Werror -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wredundant-constraints -Wunused-packages -Wno-name-shadowing -Wno-unused-matches -Wno-unused-do-bind -Wno-type-defaults 37 | - ghc: '9.8.1' 38 | 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | 43 | - uses: actions/checkout@v3 44 | 45 | - name: Setup Haskell 46 | uses: haskell-actions/setup@v2 47 | with: 48 | ghc-version: ${{ matrix.ghc }} 49 | cabal-version: 3.8 50 | 51 | - name: Generate cabal.project.freeze 52 | run: cabal freeze --enable-tests --enable-benchmarks 53 | 54 | - uses: actions/cache@v3 55 | with: 56 | path: | 57 | ~/.cabal/store 58 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 59 | restore-keys: | 60 | ${{ runner.os }}-${{ matrix.ghc }}- 61 | 62 | - name: Install deps and compile 63 | run: cabal build --enable-tests -j +RTS -A128m -n2m -N -RTS --ghc-options="${{ matrix.ghc-options }}" 64 | 65 | - name: Test 66 | run: cabal test --test-show-details always 67 | 68 | - name: Run Haddock 69 | run: cabal haddock 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 0.5.1.4 2 | 3 | - Support GHC 9.8. 4 | 5 | ## Version 0.5 6 | 7 | - Isolated the `trimming` and `untrimming` variations of quasi-quoter. 8 | 9 | ## Version 0.4 10 | 11 | - Changed the behaviour of the quasi-quoter in regards to trailing whitespace. Before it was always adding newline in the end, now it always completely removes all the trailing whitespace. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Nikita Volkov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: . 2 | 3 | if impl(ghc >= 9.8) 4 | constraints: 5 | , primitive >= 0.5 6 | , text >= 2.1 7 | 8 | allow-newer: 9 | , *:base 10 | , *:bytestring 11 | , *:deepseq 12 | , *:text 13 | -------------------------------------------------------------------------------- /library/NeatInterpolation.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- NeatInterpolation provides a quasiquoter for producing strings 3 | -- with a simple interpolation of input values. 4 | -- It removes the excessive indentation from the input and 5 | -- accurately manages the indentation of all lines of interpolated variables. 6 | -- But enough words, the code shows it better. 7 | -- 8 | -- Consider the following declaration: 9 | -- 10 | -- > {-# LANGUAGE QuasiQuotes #-} 11 | -- > 12 | -- > import NeatInterpolation 13 | -- > import Data.Text (Text) 14 | -- > 15 | -- > f :: Text -> Text -> Text 16 | -- > f a b = 17 | -- > [trimming| 18 | -- > function(){ 19 | -- > function(){ 20 | -- > $a 21 | -- > } 22 | -- > return $b 23 | -- > } 24 | -- > |] 25 | -- 26 | -- Executing the following: 27 | -- 28 | -- > main = Text.putStrLn $ f "1" "2" 29 | -- 30 | -- will produce this (notice the reduced indentation compared to how it was 31 | -- declared): 32 | -- 33 | -- > function(){ 34 | -- > function(){ 35 | -- > 1 36 | -- > } 37 | -- > return 2 38 | -- > } 39 | -- 40 | -- Now let's test it with multiline string parameters: 41 | -- 42 | -- > main = Text.putStrLn $ f 43 | -- > "{\n indented line\n indented line\n}" 44 | -- > "{\n indented line\n indented line\n}" 45 | -- 46 | -- We get 47 | -- 48 | -- > function(){ 49 | -- > function(){ 50 | -- > { 51 | -- > indented line 52 | -- > indented line 53 | -- > } 54 | -- > } 55 | -- > return { 56 | -- > indented line 57 | -- > indented line 58 | -- > } 59 | -- > } 60 | -- 61 | -- See how it neatly preserved the indentation levels of lines the 62 | -- variable placeholders were at? 63 | -- 64 | -- If you need to separate variable placeholder from the following text to 65 | -- prevent treating the rest of line as variable name, use escaped variable: 66 | -- 67 | -- > f name = [trimming|this_could_be_${name}_long_identifier|] 68 | -- 69 | -- So 70 | -- 71 | -- > f "one" == "this_could_be_one_long_identifier" 72 | -- 73 | -- If you want to write something that looks like a variable but should be 74 | -- inserted as-is, escape it with another @$@: 75 | -- 76 | -- > f word = [trimming|$$my ${word} $${string}|] 77 | -- 78 | -- results in 79 | -- 80 | -- > f "funny" == "$my funny ${string}" 81 | module NeatInterpolation (trimming, untrimming, text) where 82 | 83 | import qualified Data.Text as Text 84 | import Language.Haskell.TH 85 | import Language.Haskell.TH.Quote hiding (quoteExp) 86 | import qualified NeatInterpolation.Parsing as Parsing 87 | import NeatInterpolation.Prelude 88 | import qualified NeatInterpolation.String as String 89 | 90 | expQQ :: (String -> Q Exp) -> QuasiQuoter 91 | expQQ quoteExp = QuasiQuoter quoteExp notSupported notSupported notSupported 92 | where 93 | notSupported _ = fail "Quotation in this context is not supported" 94 | 95 | -- | 96 | -- An alias to `trimming` for backward-compatibility. 97 | text :: QuasiQuoter 98 | text = trimming 99 | 100 | -- | 101 | -- Trimmed quasiquoter variation. 102 | -- Same as `untrimming`, but also 103 | -- removes the leading and trailing whitespace. 104 | trimming :: QuasiQuoter 105 | trimming = expQQ (quoteExp . String.trim . String.unindent . String.tabsToSpaces) 106 | 107 | -- | 108 | -- Untrimmed quasiquoter variation. 109 | -- Unindents the quoted template and converts tabs to spaces. 110 | untrimming :: QuasiQuoter 111 | untrimming = expQQ (quoteExp . String.unindent . String.tabsToSpaces) 112 | 113 | indentQQPlaceholder :: Int -> Text -> Text 114 | indentQQPlaceholder indent text = case Text.lines text of 115 | head : tail -> 116 | Text.intercalate (Text.singleton '\n') $ 117 | head : map (Text.replicate indent (Text.singleton ' ') <>) tail 118 | [] -> text 119 | 120 | quoteExp :: String -> Q Exp 121 | quoteExp input = 122 | case Parsing.parseLines input of 123 | Left e -> fail $ show e 124 | Right lines -> 125 | sigE 126 | (appE [|Text.intercalate (Text.singleton '\n')|] $ listE $ map lineExp lines) 127 | [t|Text|] 128 | 129 | lineExp :: Parsing.Line -> Q Exp 130 | lineExp (Parsing.Line indent contents) = 131 | case contents of 132 | [] -> [|Text.empty|] 133 | [x] -> toExp x 134 | xs -> appE [|Text.concat|] $ listE $ map toExp xs 135 | where 136 | toExp = contentExp (fromIntegral indent) 137 | 138 | contentExp :: Integer -> Parsing.LineContent -> Q Exp 139 | contentExp _ (Parsing.LineContentText text) = appE [|Text.pack|] (stringE text) 140 | contentExp indent (Parsing.LineContentIdentifier name) = do 141 | valueName <- lookupValueName name 142 | case valueName of 143 | Just valueName -> do 144 | appE 145 | (appE (varE 'indentQQPlaceholder) $ litE $ integerL indent) 146 | (varE valueName) 147 | Nothing -> fail $ "Value `" ++ name ++ "` is not in scope" 148 | -------------------------------------------------------------------------------- /library/NeatInterpolation/Parsing.hs: -------------------------------------------------------------------------------- 1 | module NeatInterpolation.Parsing where 2 | 3 | import Data.Text (pack) 4 | import NeatInterpolation.Prelude hiding (many, some, try, (<|>)) 5 | import Text.Megaparsec 6 | import Text.Megaparsec.Char 7 | 8 | data Line = Line {lineIndent :: Int, lineContents :: [LineContent]} 9 | deriving (Show) 10 | 11 | data LineContent 12 | = LineContentText [Char] 13 | | LineContentIdentifier [Char] 14 | deriving (Show) 15 | 16 | type Parser = Parsec Void String 17 | 18 | -- | Pretty parse exception for parsing lines. 19 | newtype ParseException = ParseException Text 20 | deriving (Show, Eq) 21 | 22 | parseLines :: [Char] -> Either ParseException [Line] 23 | parseLines input = case parse lines "NeatInterpolation.Parsing.parseLines" input of 24 | Left err -> Left $ ParseException $ pack $ errorBundlePretty err 25 | Right output -> Right output 26 | where 27 | lines :: Parser [Line] 28 | lines = sepBy line newline <* eof 29 | line = Line <$> countIndent <*> many content 30 | countIndent = fmap length $ try $ lookAhead $ many $ char ' ' 31 | content = try escapedDollar <|> try identifier <|> contentText 32 | identifier = 33 | fmap LineContentIdentifier $ 34 | char '$' *> (try identifier' <|> between (char '{') (char '}') identifier') 35 | escapedDollar = fmap LineContentText $ char '$' *> count 1 (char '$') 36 | identifier' = some (alphaNumChar <|> char '\'' <|> char '_') 37 | contentText = do 38 | text <- manyTill anySingle end 39 | if null text 40 | then fail "Empty text" 41 | else return $ LineContentText $ text 42 | where 43 | end = 44 | (void $ try $ lookAhead escapedDollar) 45 | <|> (void $ try $ lookAhead identifier) 46 | <|> (void $ try $ lookAhead newline) 47 | <|> eof 48 | -------------------------------------------------------------------------------- /library/NeatInterpolation/Prelude.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | 3 | module NeatInterpolation.Prelude 4 | ( module Exports, 5 | ) 6 | where 7 | 8 | import Control.Applicative as Exports 9 | import Control.Arrow as Exports hiding (first, second) 10 | import Control.Category as Exports 11 | import Control.Concurrent as Exports 12 | import Control.Exception as Exports 13 | import Control.Monad as Exports hiding (fail, forM, forM_, mapM, mapM_, msum, sequence, sequence_) 14 | import Control.Monad.Fail as Exports 15 | import Control.Monad.Fix as Exports hiding (fix) 16 | import Control.Monad.IO.Class as Exports 17 | import Control.Monad.ST as Exports 18 | import Data.Bifunctor as Exports 19 | import Data.Bits as Exports 20 | import Data.Bool as Exports 21 | import Data.Char as Exports 22 | import Data.Coerce as Exports 23 | import Data.Complex as Exports 24 | import Data.Data as Exports 25 | import Data.Dynamic as Exports 26 | import Data.Either as Exports 27 | import Data.Fixed as Exports 28 | import Data.Foldable as Exports hiding (toList) 29 | import Data.Function as Exports hiding (id, (.)) 30 | #if MIN_VERSION_base(4,19,0) 31 | import Data.Functor as Exports hiding (unzip) 32 | #else 33 | import Data.Functor as Exports 34 | #endif 35 | import Data.Functor.Identity as Exports 36 | import Data.IORef as Exports 37 | import Data.Int as Exports 38 | import Data.Ix as Exports 39 | import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons) 40 | import Data.Maybe as Exports 41 | import Data.Monoid as Exports hiding (First (..), Last (..), (<>)) 42 | import Data.Ord as Exports 43 | import Data.Proxy as Exports 44 | import Data.Ratio as Exports 45 | import Data.STRef as Exports 46 | import Data.Semigroup as Exports 47 | import Data.String as Exports 48 | import Data.Text as Exports (Text) 49 | import Data.Traversable as Exports 50 | import Data.Tuple as Exports 51 | import Data.Unique as Exports 52 | import Data.Version as Exports 53 | import Data.Void as Exports 54 | import Data.Word as Exports 55 | import Debug.Trace as Exports 56 | import Foreign.ForeignPtr as Exports 57 | import Foreign.Ptr as Exports 58 | import Foreign.StablePtr as Exports 59 | import Foreign.Storable as Exports hiding (alignment, sizeOf) 60 | import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar) 61 | import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith) 62 | import GHC.Generics as Exports (Generic, Generic1) 63 | import GHC.IO.Exception as Exports 64 | import Numeric as Exports 65 | import System.Environment as Exports 66 | import System.Exit as Exports 67 | import System.IO as Exports 68 | import System.IO.Error as Exports 69 | import System.IO.Unsafe as Exports 70 | import System.Mem as Exports 71 | import System.Mem.StableName as Exports 72 | import System.Timeout as Exports 73 | import Text.Printf as Exports (hPrintf, printf) 74 | import Text.Read as Exports (Read (..), readEither, readMaybe) 75 | import Unsafe.Coerce as Exports 76 | import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.)) 77 | -------------------------------------------------------------------------------- /library/NeatInterpolation/String.hs: -------------------------------------------------------------------------------- 1 | module NeatInterpolation.String where 2 | 3 | import NeatInterpolation.Prelude 4 | 5 | unindent :: [Char] -> [Char] 6 | unindent s = 7 | case lines s of 8 | head : tail -> 9 | let unindentedHead = dropWhile (== ' ') head 10 | minimumTailIndent = minimumIndent . unlines $ tail 11 | unindentedTail = case minimumTailIndent of 12 | Just indent -> map (drop indent) tail 13 | Nothing -> tail 14 | in unlines $ unindentedHead : unindentedTail 15 | [] -> [] 16 | 17 | trim :: [Char] -> [Char] 18 | trim = dropWhileRev isSpace . dropWhile isSpace 19 | 20 | dropWhileRev :: (a -> Bool) -> [a] -> [a] 21 | dropWhileRev p = foldr (\x xs -> if p x && null xs then [] else x : xs) [] 22 | 23 | tabsToSpaces :: [Char] -> [Char] 24 | tabsToSpaces ('\t' : tail) = " " ++ tabsToSpaces tail 25 | tabsToSpaces (head : tail) = head : tabsToSpaces tail 26 | tabsToSpaces [] = [] 27 | 28 | minimumIndent :: [Char] -> Maybe Int 29 | minimumIndent = 30 | listToMaybe 31 | . sort 32 | . map lineIndent 33 | . filter (not . null . dropWhile isSpace) 34 | . lines 35 | 36 | -- | Amount of preceding spaces on first line 37 | lineIndent :: [Char] -> Int 38 | lineIndent = length . takeWhile (== ' ') 39 | -------------------------------------------------------------------------------- /neat-interpolation.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: neat-interpolation 3 | version: 0.5.1.4 4 | synopsis: 5 | Quasiquoter for neat and simple multiline text interpolation 6 | 7 | description: 8 | Quasiquoter for producing Text values with support for 9 | a simple interpolation of input values. 10 | It removes the excessive indentation from the input and 11 | accurately manages the indentation of all lines of the interpolated variables. 12 | 13 | category: String, QuasiQuotes 14 | license: MIT 15 | license-file: LICENSE 16 | copyright: (c) 2013, Nikita Volkov 17 | author: Nikita Volkov 18 | maintainer: Nikita Volkov 19 | homepage: https://github.com/nikita-volkov/neat-interpolation 20 | bug-reports: https://github.com/nikita-volkov/neat-interpolation/issues 21 | build-type: Simple 22 | extra-source-files: CHANGELOG.md 23 | 24 | source-repository head 25 | type: git 26 | location: git://github.com/nikita-volkov/neat-interpolation.git 27 | 28 | library 29 | hs-source-dirs: library 30 | default-extensions: 31 | NoImplicitPrelude 32 | NoMonomorphismRestriction 33 | BangPatterns 34 | BinaryLiterals 35 | ConstraintKinds 36 | DataKinds 37 | DefaultSignatures 38 | DeriveDataTypeable 39 | DeriveFoldable 40 | DeriveFunctor 41 | DeriveGeneric 42 | DeriveTraversable 43 | DuplicateRecordFields 44 | EmptyDataDecls 45 | FlexibleContexts 46 | FlexibleInstances 47 | FunctionalDependencies 48 | GADTs 49 | GeneralizedNewtypeDeriving 50 | LambdaCase 51 | LiberalTypeSynonyms 52 | MagicHash 53 | MultiParamTypeClasses 54 | MultiWayIf 55 | OverloadedLists 56 | OverloadedStrings 57 | ParallelListComp 58 | PatternGuards 59 | PatternSynonyms 60 | QuasiQuotes 61 | RankNTypes 62 | RecordWildCards 63 | ScopedTypeVariables 64 | StandaloneDeriving 65 | StrictData 66 | TemplateHaskell 67 | TupleSections 68 | TypeApplications 69 | TypeFamilies 70 | TypeOperators 71 | UnboxedTuples 72 | 73 | default-language: Haskell2010 74 | exposed-modules: NeatInterpolation 75 | other-modules: 76 | NeatInterpolation.Parsing 77 | NeatInterpolation.Prelude 78 | NeatInterpolation.String 79 | 80 | build-depends: 81 | , base >=4.9 && <5 82 | , megaparsec >=7 && <10 83 | , template-haskell >=2.8 && <3 84 | , text >=1 && <3 85 | 86 | test-suite test 87 | type: exitcode-stdio-1.0 88 | hs-source-dirs: test 89 | default-extensions: 90 | NoImplicitPrelude 91 | NoMonomorphismRestriction 92 | BangPatterns 93 | BinaryLiterals 94 | ConstraintKinds 95 | DataKinds 96 | DefaultSignatures 97 | DeriveDataTypeable 98 | DeriveFoldable 99 | DeriveFunctor 100 | DeriveGeneric 101 | DeriveTraversable 102 | DuplicateRecordFields 103 | EmptyDataDecls 104 | FlexibleContexts 105 | FlexibleInstances 106 | FunctionalDependencies 107 | GADTs 108 | GeneralizedNewtypeDeriving 109 | LambdaCase 110 | LiberalTypeSynonyms 111 | MagicHash 112 | MultiParamTypeClasses 113 | MultiWayIf 114 | OverloadedLists 115 | OverloadedStrings 116 | ParallelListComp 117 | PatternGuards 118 | PatternSynonyms 119 | QuasiQuotes 120 | RankNTypes 121 | RecordWildCards 122 | ScopedTypeVariables 123 | StandaloneDeriving 124 | StrictData 125 | TemplateHaskell 126 | TupleSections 127 | TypeApplications 128 | TypeFamilies 129 | TypeOperators 130 | UnboxedTuples 131 | 132 | default-language: Haskell2010 133 | main-is: Main.hs 134 | build-depends: 135 | , neat-interpolation 136 | , rerebase <2 137 | , tasty >=1.2.3 && <2 138 | , tasty-hunit >=0.10.0.2 && <0.11 139 | -------------------------------------------------------------------------------- /test/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import NeatInterpolation 4 | import Test.Tasty 5 | import Test.Tasty.HUnit 6 | import Prelude hiding (choose) 7 | 8 | main :: IO () 9 | main = 10 | defaultMain $ 11 | testGroup "" $ 12 | [ testCase "Demo" $ 13 | let template a b = 14 | [trimming| 15 | function(){ 16 | function(){ 17 | $a 18 | } 19 | return $b 20 | } 21 | |] 22 | a = "{\n indented line\n indented line\n}" 23 | in assertEqual 24 | "" 25 | "function(){\n function(){\n {\n indented line\n indented line\n }\n }\n return {\n indented line\n indented line\n }\n}" 26 | (template a a), 27 | testCase "Isolation" $ 28 | let isolated name = [trimming|this_could_be_${name}_long_identifier|] 29 | in assertEqual 30 | "" 31 | "this_could_be_one_long_identifier" 32 | (isolated "one"), 33 | testCase "Escaping 1" $ 34 | let template a b = 35 | [trimming| 36 | function(){ 37 | function(){ 38 | $a 39 | } 40 | return "$$b" 41 | } 42 | |] 43 | a = "{\n indented line\n indented line\n}" 44 | in assertEqual 45 | "" 46 | "function(){\n function(){\n {\n indented line\n indented line\n }\n }\n return \"$b\"\n}" 47 | (template a a), 48 | testCase "Escaping 2" $ 49 | let escaped name = [trimming|this_could_be_$$${name}$$_long_identifier|] 50 | in assertEqual 51 | "" 52 | "this_could_be_$one$_long_identifier" 53 | (escaped "one"), 54 | testCase "Deindentation" $ 55 | let template fieldName className = 56 | [trimming| 57 | * @param $fieldName value of the {@code $fieldName} property of 58 | the {@code $className} case 59 | |] 60 | in assertEqual 61 | "" 62 | "* @param a value of the {@code a} property of\n the {@code b} case" 63 | (template "a" "b") 64 | ] 65 | --------------------------------------------------------------------------------