{-# LANGUAGE ScopedTypeVariables #-} -- | -- License: GPL-2.0-or-later AND BSD-3-Clause -- -- Zinza - a small jinja-syntax-inspired typed-template compiler. -- -- Zinza typechecks and compiles a template. -- We can compile either to Haskell function, or to verbatim Haskell module (planned). -- -- Zinza is very minimalistic. Features are added when needed. -- -- == Example usage -- -- Given a template -- -- @ -- {% for license in licenses %} -- licenseName {{license.con}} = {{license.name}} -- {% endfor %} -- @ -- -- and data definitions like: -- -- @ -- newtype Licenses = Licenses { licenses :: [License] } -- deriving ('GHC.Generics.Generic') -- -- data License = License -- { licenseCon :: String -- , licenseName :: String -- } -- deriving ('GHC.Generics.Generic') -- @ -- -- We can (generically) derive 'Zinza' instances for @Licenses@ and @License@ -- -- @ -- instance 'Zinza' Licenses where -- 'toType' = 'genericToType' id -- 'toValue' = 'genericToValue' id -- 'fromValue' = 'genericFromValue' id -- -- instance 'Zinza' License where -- 'toType' = 'genericToTypeSFP' -- 'toValue' = 'genericToValueSFP' -- 'fromValue' = 'genericFromValueSFP' -- @ -- -- Then the example of run-time usage is -- -- @ -- example :: IO String -- example = do -- -- this might fail, type errors! -- run <- 'parseAndCompileTemplateIO' "fixtures/licenses.zinza" -- -- this shouldn't fail (run-time errors are due bugs in zinza) -- run $ Licenses -- [ License \"Foo" (show "foo-1.0") -- , License \"Bar" (show "bar-1.2") -- ] -- @ -- -- The result of running an @example@ is: -- -- @ -- licenseName Foo = "foo-1.0" -- licenseName Bar = "bar-1.2" -- @ -- -- == Module generation -- -- Zinza also supports standalone module generation. -- -- @ -- 'parseAndCompileModuleIO' ('simpleConfig' \"DemoLicenses\" [\"Licenses\"] :: 'ModuleConfig' Licenses) "fixtures/licenses.zinza" >>= putStr -- @ -- -- prints a Haskell module source code: -- -- @ -- {-# OPTIONS_GHC -fno-warn-unused-imports #-} -- module DemoLicenses (render) where -- import Prelude (String, fst, snd, ($), not, return) -- import Control.Monad (forM_) -- import Licenses -- type Writer a = (String, a) -- tell :: String -> Writer (); tell x = (x, ()) -- execWriter :: Writer a -> String; execWriter = fst -- render :: Licenses -> String -- render (z_root) = execWriter $ do -- forM_ (licenses $ z_root) $ \z_var0_license -> do -- tell "licenseName " -- tell (licenseCon $ z_var0_license) -- tell " = " -- tell (licenseName $ z_var0_license) -- tell "\n" -- @ -- -- which is not dependent on Zinza. You are free to use more efficient writer -- as well. -- -- === Expressions -- -- @ -- {{ expression }} -- @ -- -- Expression syntax has only few constructions: -- -- * field access @foo.bar@ -- -- * function application @fun bar@ (though function can only be @not@) -- -- /Note:/ you can provide your own /Prelude/ of functions. See @Bools.hs@ -- and @Bools.zinza@ in tests for an example. -- You cannot define new functions in templates, but you can pass -- them as template arguments. -- -- === Control structures -- -- The __for__ and __if__ statements are supported: -- -- @ -- {% for value in values %} -- ... -- {% endfor %} -- @ -- -- @ -- {% if boolExpression %} -- ... -- {% elif anotherBoolExpression %} -- ... -- {% else %} -- ... -- {% endif %} -- @ -- -- If a control structure tag starts at the first column, the possible -- trailing new line feed is stripped. This way full-line control tags -- don't introduce new lines in the output. -- -- === Blocks -- -- It's possible to define blocks to be used (possibly multiple times) later: -- -- @ -- {% defblock blockname %} -- ... -- {% endblock %} -- @ -- -- And the block can be used later with: -- -- @ -- {% useblock blockname %} -- @ -- -- Blocks follow scopes of @if@ and @for@ control structures -- -- === Comments -- -- @ -- {\# Comments are omitted from the output #} -- @ -- module Zinza ( parseAndCompileTemplate, parseAndCompileTemplateIO, -- * Compilation to Haskell module parseAndCompileModule, parseAndCompileModuleIO, ModuleConfig (..), simpleConfig, -- * Input class Zinza (..), -- ** Generic deriving genericToType, genericToValue, genericFromValue, genericToTypeSFP, genericToValueSFP, genericFromValueSFP, stripFieldPrefix, GZinzaType, GZinzaValue, GZinzaFrom, GFieldNames, -- * Templates Node (..), Nodes, Expr (..), LExpr, -- * Types -- | Zinza's type-system is delibarately extremely simple. Ty (..), displayTy, -- * Values -- | 'Value's are passed at run-time, when the template is interpreted. -- When compiled to the Haskell module, 'Value's aren't used. Value (..), -- * Errors ParseError (..), CompileError (..), CompileOrParseError (..), RuntimeError (..), AsRuntimeError (..), ThrowRuntime (..), -- * Location Loc (..), Located (..), zeroLoc, displayLoc, TraversableWithLoc (..), -- * Variables Var, Selector, ) where import Control.Exception (throwIO) import Data.Typeable (Typeable, typeRep) import Zinza.Check import Zinza.Errors import Zinza.Expr import Zinza.Generic import Zinza.Module import Zinza.Node import Zinza.Parser import Zinza.Pos import Zinza.Type import Zinza.Value import Zinza.Var -- | Parse and compile the template into Haskell function. parseAndCompileTemplate :: (Zinza a, ThrowRuntime m) => FilePath -- ^ name of the template -> String -- ^ contents of the template -> Either CompileOrParseError (a -> m String) parseAndCompileTemplate name contents = case parseTemplate name contents of Left err -> Left (AParseError err) Right nodes -> case check nodes of Left err' -> Left (ACompileError err') Right res -> Right res -- | Like 'parseAndCompileTemplate' but reads file and (possibly) -- throws 'CompileOrParseError'. parseAndCompileTemplateIO :: (Zinza a, ThrowRuntime m) => FilePath -> IO (a -> m String) parseAndCompileTemplateIO name = do contents <- readFile name either throwIO return $ parseAndCompileTemplate name contents -- | Parse and compile the template into 'String' representing a Haskell module. parseAndCompileModule :: Zinza a => ModuleConfig a -> FilePath -> String -> Either CompileOrParseError String parseAndCompileModule mc name contents = case parseTemplate name contents of Left err -> Left (AParseError err) Right nodes -> case checkModule mc nodes of Left err -> Left (ACompileError err) Right res -> Right res -- | Like 'parseAndCompileModule' but reads file and (possibly) -- throws 'CompileOrParseError'. parseAndCompileModuleIO :: Zinza a => ModuleConfig a -> FilePath -> IO String parseAndCompileModuleIO mc name = do contents <- readFile name either throwIO return $ parseAndCompileModule mc name contents -- | Simple configuration to use with 'parseAndCompileModule' or -- 'parseAndCompileModuleIO'. simpleConfig :: forall a. Typeable a => String -- ^ module name -> [String] -- ^ imports -> ModuleConfig a simpleConfig moduleName imports = ModuleConfig { mcRender = "render" , mcHeader = [ "{-# OPTIONS_GHC -fno-warn-unused-imports #-}" , "module " ++ moduleName ++ " (render) where" , "import Prelude (String, fst, snd, ($), return)" , "import Control.Monad (forM_)" ] ++ [ "import " ++ i | i <- imports ] ++ [ "type Writer a = (String, a)" , "tell :: String -> Writer (); tell x = (x, ())" , "execWriter :: Writer a -> String; execWriter = fst" , "render :: " ++ typeName ++ " -> String" ] } where typeName = show (typeRep ([] :: [a]))