packages feed

doctemplates (empty) → 0.1.0.0

raw patch · 6 files changed

+489/−0 lines, 6 filesdep +aesondep +basedep +blaze-htmlsetup-changed

Dependencies added: aeson, base, blaze-html, blaze-markup, bytestring, containers, doctemplates, hspec, parsec, scientific, text, unordered-containers, vector

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright John MacFarlane (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,64 @@+# doctemplates++This is the templating system used by pandoc.  It was formerly+be a module in pandoc. It has been split off to make it easier+to use independently.++Example:++``` haskell+{-# LANGUAGE OverloadedStrings #-}+import Data.Text+import Data.Aeson+import Text.DocTemplates++data Employee = Employee { firstName :: String+                         , lastName  :: String+                         , salary    :: Maybe Int }+instance ToJSON Employee where+  toJSON e = object [ "name" .= object [ "first" .= firstName e+                                       , "last"  .= lastName e ]+                    , "salary" .= salary e ]++template :: Text+template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"++main = case compileTemplate template of+         Left e    -> error e+         Right t   -> putStrLn $ renderTemplate t $ object+                        ["employee" .=+                          [ Employee "John" "Doe" Nothing+                          , Employee "Omar" "Smith" (Just 30000)+                          , Employee "Sara" "Chen" (Just 60000) ]+                        ]+```++A slot for an interpolated variable is a variable name surrounded+by dollar signs.  To include a literal `$` in your template, use+`$$`.  Variable names must begin with a letter and can contain letters,+numbers, `_`, `-`, and `.`.++The values of variables are determined by a JSON object that is+passed as a parameter to `renderTemplate`.  So, for example,+`title` will return the value of the `title` field, and+`employee.salary` will return the value of the `salary` field+of the object that is the value of the `employee` field.++The value of a variable will be indented to the same level as the+variable.++A conditional begins with `$if(variable_name)$` and ends with `$endif$`.+It may optionally contain an `$else$` section.  The if section is+used if `variable_name` has a non-null value, otherwise the else section+is used.++Conditional keywords should not be indented, or unexpected spacing+problems may occur.++The `$for$` keyword can be used to iterate over an array.  If+the value of the associated variable is not an array, a single+iteration will be performed on its value.++You may optionally specify separators using `$sep$`, as in the+example above.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ doctemplates.cabal view
@@ -0,0 +1,47 @@+name:                doctemplates+version:             0.1.0.0+synopsis:            Pandoc-style document templates+description:         Please see README.md+homepage:            https://github.com/jgm/doctemplates#readme+license:             BSD3+license-file:        LICENSE+author:              John MacFarlane+maintainer:          jgm@berkeley.edu+copyright:           2016 John MacFarlane+category:            Text+build-type:          Simple+-- extra-source-files:+data-files:          README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Text.DocTemplates+  build-depends:       base >= 4.7 && < 5,+                       aeson,+                       bytestring,+                       blaze-markup,+                       blaze-html,+                       text,+                       containers,+                       vector,+                       parsec,+                       unordered-containers,+                       scientific+  default-language:    Haskell2010++test-suite doctemplates-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       base,+                       doctemplates,+                       aeson,+                       hspec,+                       text+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/jgm/doctemplates
+ src/Text/DocTemplates.hs view
@@ -0,0 +1,305 @@+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances,+    OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables #-}+{- |+   Module      : Text.Pandoc.Templates+   Copyright   : Copyright (C) 2009-2016 John MacFarlane+   License     : BSD3++   Maintainer  : John MacFarlane <jgm@berkeley.edu>+   Stability   : alpha+   Portability : portable++A simple templating system with variable substitution and conditionals.+This module was formerly part of pandoc and is used for pandoc's+templates.  The following program illustrates its use:++> {-# LANGUAGE OverloadedStrings #-}+> import Data.Text+> import Data.Aeson+> import Text.DocTemplates+>+> data Employee = Employee { firstName :: String+>                          , lastName  :: String+>                          , salary    :: Maybe Int }+> instance ToJSON Employee where+>   toJSON e = object [ "name" .= object [ "first" .= firstName e+>                                        , "last"  .= lastName e ]+>                     , "salary" .= salary e ]+>+> template :: Text+> template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"+>+> main = case compileTemplate template of+>          Left e    -> error e+>          Right t   -> putStrLn $ renderTemplate t $ object+>                         ["employee" .=+>                           [ Employee "John" "Doe" Nothing+>                           , Employee "Omar" "Smith" (Just 30000)+>                           , Employee "Sara" "Chen" (Just 60000) ]+>                         ]++A slot for an interpolated variable is a variable name surrounded+by dollar signs.  To include a literal @$@ in your template, use+@$$@.  Variable names must begin with a letter and can contain letters,+numbers, @_@, @-@, and @.@.++The values of variables are determined by a JSON object that is+passed as a parameter to @renderTemplate@.  So, for example,+@title@ will return the value of the @title@ field, and+@employee.salary@ will return the value of the @salary@ field+of the object that is the value of the @employee@ field.++The value of a variable will be indented to the same level as the+variable.++A conditional begins with @$if(variable_name)$@ and ends with @$endif$@.+It may optionally contain an @$else$@ section.  The if section is+used if @variable_name@ has a non-null value, otherwise the else section+is used.++Conditional keywords should not be indented, or unexpected spacing+problems may occur.++The @$for$@ keyword can be used to iterate over an array.  If+the value of the associated variable is not an array, a single+iteration will be performed on its value.++You may optionally specify separators using @$sep$@, as in the+example above.++-}++module Text.DocTemplates ( renderTemplate+                         , applyTemplate+                         , TemplateTarget(..)+                         , varListToJSON+                         , compileTemplate+                         , Template+                         ) where++import Data.Char (isAlphaNum)+import Control.Monad (guard, when)+import Data.Aeson (ToJSON(..), Value(..))+import qualified Text.Parsec as P+import Text.Parsec.Text (Parser)+import qualified Data.Set as Set+import Data.Monoid ((<>))+import qualified Data.Text as T+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Data.List (intersperse)+import qualified Data.Map as M+import qualified Data.HashMap.Strict as H+import Data.Foldable (toList)+import Text.Blaze.Html (Html)+import Text.Blaze.Internal (preEscapedText)+import Data.ByteString.Lazy (ByteString, fromChunks)+import Data.Vector ((!?))+import Control.Applicative (many, (<|>))+import Data.Scientific (floatingOrInteger)++-- | A 'Template' is essentially a function that takes+-- a JSON 'Value' and produces 'Text'.+newtype Template = Template { unTemplate :: Value -> Text }+                 deriving Monoid++type Variable = [Text]++class TemplateTarget a where+  toTarget :: Text -> a++instance TemplateTarget Text where+  toTarget = id++instance TemplateTarget String where+  toTarget = T.unpack++instance TemplateTarget ByteString where+  toTarget = fromChunks . (:[]) . encodeUtf8++instance TemplateTarget Html where+  toTarget = preEscapedText++-- | A convenience function for passing in an association+-- list of string values instead of a JSON 'Value'.+varListToJSON :: [(String, String)] -> Value+varListToJSON assoc = toJSON $ M.fromList assoc'+  where assoc' = [(T.pack k, toVal [T.pack z | (y,z) <- assoc,+                                                not (null z),+                                                y == k])+                        | k <- ordNub $ map fst assoc ]+        toVal [x] = toJSON x+        toVal []  = Null+        toVal xs  = toJSON xs++-- An efficient specialization of nub.+ordNub :: (Ord a) => [a] -> [a]+ordNub l = go Set.empty l+  where+    go _ [] = []+    go s (x:xs) = if x `Set.member` s then go s xs+                                      else x : go (Set.insert x s) xs++-- | Compile a template.+compileTemplate :: Text -> Either String Template+compileTemplate template =+  case P.parse (pTemplate <* P.eof) "template" template of+       Left e   -> Left (show e)+       Right x  -> Right x++-- | Render a compiled template using @context@ to resolve variables.+renderTemplate :: (ToJSON a, TemplateTarget b) => Template -> a -> b+renderTemplate (Template f) context = toTarget $ f $ toJSON context++-- | Combines `renderTemplate` and `compileTemplate`.+applyTemplate :: (ToJSON a, TemplateTarget b) => Text -> a -> Either String b+applyTemplate t context =+  case compileTemplate t of+         Left e   -> Left e+         Right f  -> Right $ renderTemplate f context++var :: Variable -> Template+var = Template . resolveVar++resolveVar :: Variable -> Value -> Text+resolveVar var' val =+  case multiLookup var' val of+       Just (Array vec) -> maybe mempty (resolveVar []) $ vec !? 0+       Just (String t)  -> T.stripEnd t+       Just (Number n)  -> case floatingOrInteger n of+                                   Left (r :: Double)   -> T.pack $ show r+                                   Right (i :: Integer) -> T.pack $ show i+       Just (Bool True) -> "true"+       Just (Object _)  -> "true"+       Just _           -> mempty+       Nothing          -> mempty++multiLookup :: [Text] -> Value -> Maybe Value+multiLookup [] x = Just x+multiLookup (v:vs) (Object o) = H.lookup v o >>= multiLookup vs+multiLookup _ _ = Nothing++lit :: Text -> Template+lit = Template . const++cond :: Variable -> Template -> Template -> Template+cond var' (Template ifyes) (Template ifno) = Template $ \val ->+  case resolveVar var' val of+       "" -> ifno val+       _  -> ifyes val++iter :: Variable -> Template -> Template -> Template+iter var' template sep = Template $ \val -> unTemplate+  (case multiLookup var' val of+           Just (Array vec) -> mconcat $ intersperse sep+                                       $ map (setVar template var')+                                       $ toList vec+           Just x           -> cond var' (setVar template var' x) mempty+           Nothing          -> mempty) val++setVar :: Template -> Variable -> Value -> Template+setVar (Template f) var' val = Template $ f . replaceVar var' val++replaceVar :: Variable -> Value -> Value -> Value+replaceVar []     new _          = new+replaceVar (v:vs) new (Object o) =+  Object $ H.adjust (\x -> replaceVar vs new x) v o+replaceVar _ _ old = old++--- parsing++pTemplate :: Parser Template+pTemplate = do+  sp <- P.option mempty pInitialSpace+  rest <- mconcat <$> many (pConditional <|>+                            pFor <|>+                            pNewline <|>+                            pVar <|>+                            pLit <|>+                            pEscapedDollar)+  return $ sp <> rest++takeWhile1 :: (Char -> Bool) -> Parser Text+takeWhile1 f = T.pack <$> P.many1 (P.satisfy f)++pLit :: Parser Template+pLit = lit <$> takeWhile1 (\x -> x /='$' && x /= '\n')++pNewline :: Parser Template+pNewline = do+  P.char '\n'+  sp <- P.option mempty pInitialSpace+  return $ lit "\n" <> sp++pInitialSpace :: Parser Template+pInitialSpace = do+  sps <- takeWhile1 (==' ')+  let indentVar = if T.null sps+                     then id+                     else indent (T.length sps)+  v <- P.option mempty $ indentVar <$> pVar+  return $ lit sps <> v++pEscapedDollar :: Parser Template+pEscapedDollar = lit "$" <$ P.try (P.string "$$")++pVar :: Parser Template+pVar = var <$> (P.try $ P.char '$' *> pIdent <* P.char '$')++pIdent :: Parser [Text]+pIdent = do+  first <- pIdentPart+  rest <- many (P.char '.' *> pIdentPart)+  return (first:rest)++pIdentPart :: Parser Text+pIdentPart = P.try $ do+  first <- P.letter+  rest <- T.pack <$> P.many (P.satisfy (\c -> isAlphaNum c || c == '_' || c == '-'))+  let id' = T.singleton first <> rest+  guard $ id' `notElem` reservedWords+  return id'++reservedWords :: [Text]+reservedWords = ["else","endif","for","endfor","sep"]++skipEndline :: Parser ()+skipEndline = P.try $ P.skipMany (P.satisfy (`elem` (" \t" :: String))) >> P.char '\n' >> return ()++pConditional :: Parser Template+pConditional = do+  P.try $ P.string "$if("+  id' <- pIdent+  P.string ")$"+  -- if newline after the "if", then a newline after "endif" will be swallowed+  multiline <- P.option False (True <$ skipEndline)+  ifContents <- pTemplate+  elseContents <- P.option mempty $ P.try $+                      do P.string "$else$"+                         when multiline $ P.option () skipEndline+                         pTemplate+  P.string "$endif$"+  when multiline $ P.option () skipEndline+  return $ cond id' ifContents elseContents++pFor :: Parser Template+pFor = do+  P.try $ P.string "$for("+  id' <- pIdent+  P.string ")$"+  -- if newline after the "for", then a newline after "endfor" will be swallowed+  multiline <- P.option False $ skipEndline >> return True+  contents <- pTemplate+  sep <- P.option mempty $+           do P.try $ P.string "$sep$"+              when multiline $ P.option () skipEndline+              pTemplate+  P.string "$endfor$"+  when multiline $ P.option () skipEndline+  return $ iter id' contents sep++indent :: Int -> Template -> Template+indent 0   x            = x+indent ind (Template f) = Template $ \val -> indent' (f val)+  where indent' t = T.concat+                    $ intersperse ("\n" <> T.replicate ind " ") $ T.lines t
+ test/Spec.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings #-}++import Text.DocTemplates+import Test.Hspec+import Data.Text+import Data.Aeson++data Employee = Employee { firstName :: String+                         , lastName  :: String+                         , salary    :: Maybe Integer }+instance ToJSON Employee where+  toJSON e = object [ "name" .= object [ "first" .= firstName e+                                       , "last"  .= lastName e ]+                    , "salary" .= salary e ]++employees :: [Employee]+employees = [ Employee "John" "Doe" Nothing+            , Employee "Omar" "Smith" (Just 30000)+            , Employee "Sara" "Chen" (Just 60000) ]++template :: Text+template =+  "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $$$employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"++main :: IO ()+main = hspec $ do+  describe "applyTemplate" $ do+    it "works" $ do+      applyTemplate template (object ["employee" .= employees])+        `shouldBe`+        (Right "Hi, John. No salary data.\nHi, Omar. You make $30000.\nHi, Sara. You make $60000." :: Either String Text)+    it "renders numbers appropriately as integer or floating" $ do+      applyTemplate "$m$ and $n$"+        (object ["m" .= (5 :: Integer), "n" .= (7.3 :: Double)])+        `shouldBe`+        (Right "5 and 7.3" :: Either String Text)+    it "fails with an incorrect template" $ do+      applyTemplate "$if(x$and$endif$" (object [])+        `shouldBe`+        (Left "\"template\" (line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")$\"" :: Either String Text)+