ginger 0.5.3.0 → 0.6.0.0
raw patch · 9 files changed
+227/−25 lines, 9 files
Files
- CHANGELOG.md +10/−0
- cli/GingerCLI.hs +1/−1
- ginger.cabal +1/−1
- src/Text/Ginger/AST.hs +10/−0
- src/Text/Ginger/Parse.hs +50/−0
- src/Text/Ginger/Run.hs +39/−11
- src/Text/Ginger/Run/Builtins.hs +12/−8
- src/Text/Ginger/Run/Type.hs +67/−4
- test/Text/Ginger/SimulationTests.hs +37/−0
CHANGELOG.md view
@@ -1,3 +1,13 @@+## 0.6.0.0++- Exceptions / exception handling.++## 0.5.3.0++- Marshalling and hoisting: it is now possible to fully marshal `GVal`s between+ arbitrary carrier monads, as long as suitable conversion functions are+ provided.+ ## 0.5.2.0 - Added map(), upper(), lower() functions
cli/GingerCLI.hs view
@@ -84,7 +84,7 @@ printParserError tplSource err Right t -> do let context = makeContextHtmlM contextLookup (putStr . Text.unpack . htmlSource)- runGingerT context t >>= hPutStrLn stderr . show+ runGingerT context t >>= either (hPutStrLn stderr . show) (putStr . show) printParserError :: Maybe String -> ParserError -> IO () printParserError srcMay = putStrLn . formatParserError srcMay
ginger.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: ginger-version: 0.5.3.0+version: 0.6.0.0 synopsis: An implementation of the Jinja2 template language in Haskell description: Ginger is Jinja, minus the most blatant pythonisms. Wants to be feature complete, but isn't quite there yet.
src/Text/Ginger/AST.hs view
@@ -48,7 +48,17 @@ | BlockRefS VarName | PreprocessedIncludeS Template -- ^ {% include "template" %} | NullS -- ^ The do-nothing statement (NOP)+ | TryCatchS Statement [CatchBlock] Statement -- ^ Try / catch / finally deriving (Show)++-- | A @catch@ block+data CatchBlock =+ Catch+ { catchWhat :: Maybe Text+ , catchCaptureAs :: Maybe VarName+ , catchBody :: Statement+ }+ deriving (Show) -- | Expressions, building blocks for the expression minilanguage. data Expression
src/Text/Ginger/Parse.hs view
@@ -242,6 +242,7 @@ statementP :: Monad m => Parser m Statement statementP = interpolationStmtP <|> commentStmtP+ <|> tryCatchStmtP <|> ifStmtP <|> switchStmtP <|> setStmtP@@ -408,6 +409,55 @@ spacesOrComment falseStmt <- scriptElifP <|> scriptElseP <|> return NullS return $ IfS condExpr trueStmt falseStmt++tryCatchStmtP :: Monad m => Parser m Statement+tryCatchStmtP = do+ try $ simpleTagP "try"+ tryS <- statementsP+ catchesS <- many catchBranchP+ finallyS <- finallyBranchP <|> return NullS+ simpleTagP "endtry"+ return $ TryCatchS tryS catchesS finallyS++catchBranchP :: Monad m => Parser m CatchBlock+catchBranchP = do+ (what, captureName) <- try $+ fancyTagP "catch" (try catchHeaderP <|> return (Nothing, Nothing))+ body <- statementsP+ return $ Catch what captureName body++suchThat :: Monad m => (a -> Bool) -> Parser m a -> Parser m a+suchThat p action = do+ val <- action+ if p val then return val else fail "Requirement not met"++catchHeaderP :: Monad m => Parser m (Maybe Text, Maybe VarName)+catchHeaderP = do+ spaces+ what <- catchWhatP+ spaces+ captureName <- catchCaptureP+ return $ (what, captureName)++catchWhatP :: Monad m => Parser m (Maybe Text)+catchWhatP =+ (Nothing <$ char '*') <|>+ (Just . Text.pack <$> try stringLiteralP) <|>+ (Just <$> try identifierP)++catchCaptureP :: Monad m => Parser m (Maybe VarName)+catchCaptureP = optionMaybe $ do+ try (string "as" >> notFollowedBy identCharP)+ spaces+ identifierP++finallyBranchP :: Monad m => Parser m Statement+finallyBranchP = do+ try $ simpleTagP "finally"+ statementsP++-- TODO: try/catch/finally in script mode+ switchStmtP :: Monad m => Parser m Statement switchStmtP = do
src/Text/Ginger/Run.hs view
@@ -86,7 +86,8 @@ import Text.Ginger.Run.VM import Text.Printf import Text.PrintfA-import Text.Ginger.Parse (parseGinger)+import Text.Ginger.Parse (parseGinger, ParserError)+import Control.Monad.Except (runExceptT, throwError, catchError) import Data.Text (Text) import Data.String (fromString)@@ -174,7 +175,10 @@ , ToGVal (Run m h) v , ToGVal (Run m h) h )- => (h -> m ()) -> v -> Template -> m (GVal (Run m h))+ => (h -> m ())+ -> v+ -> Template+ -> m (Either RuntimeError (GVal (Run m h))) easyRenderM emit context template = runGingerT (easyContext emit context) template @@ -197,15 +201,20 @@ -- | Purely expand a Ginger template. The underlying carrier monad is 'Writer' -- 'h', which is used to collect the output and render it into a 'h' -- value.-runGinger :: (ToGVal (Run (Writer h) h) h, Monoid h) => GingerContext (Writer h) h -> Template -> h-runGinger context template = execWriter $ runGingerT context template+runGinger :: (ToGVal (Run (Writer h) h) h, Monoid h)+ => GingerContext (Writer h) h+ -> Template+ -> h+runGinger context template =+ execWriter $ runGingerT context template -- | Monadically run a Ginger template. The @m@ parameter is the carrier monad. runGingerT :: (ToGVal (Run m h) h, Monoid h, Monad m, Functor m) => GingerContext m h -> Template- -> m (GVal (Run m h))-runGingerT context tpl = runReaderT (evalStateT (runTemplate tpl) (defRunState tpl)) context+ -> m (Either RuntimeError (GVal (Run m h)))+runGingerT context tpl =+ runReaderT (evalStateT (runExceptT (runTemplate tpl)) (defRunState tpl)) context -- | Find the effective base template of an inheritance chain baseTemplate :: Template -> Template@@ -218,7 +227,8 @@ runTemplate :: (ToGVal (Run m h) h, Monoid h, Monad m, Functor m) => Template -> Run m h (GVal (Run m h))-runTemplate = runStatement . templateBody . baseTemplate+runTemplate =+ runStatement . templateBody . baseTemplate -- | Run an action within a different template context. withTemplate :: (Monad m, Functor m)@@ -250,7 +260,7 @@ tpl <- gets rsCurrentTemplate let blockMay = resolveBlock blockName tpl case blockMay of- Nothing -> fail $ "Block " <> Text.unpack blockName <> " not defined"+ Nothing -> throwError $ UndefinedBlockError blockName Just block -> return block where resolveBlock :: VarName -> Template -> Maybe Block@@ -348,7 +358,7 @@ . fmap snd $ args loop :: [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))- loop [] = fail "Invalid call to `loop`; at least one argument is required"+ loop [] = throwError $ ArgumentsError "loop" "at least one argument is required" loop ((_, loopee):_) = go (Prelude.succ recursionDepth) loopee iteration :: (Int, (GVal (Run m h), GVal (Run m h))) -> Run m h (GVal (Run m h))@@ -379,6 +389,24 @@ runStatement (PreprocessedIncludeS tpl) = withTemplate tpl $ runTemplate tpl +runStatement (TryCatchS tryS catchesS finallyS) = do+ result <- (runStatement tryS) `catchError` handle catchesS+ runStatement finallyS+ return result+ where+ handle [] e = return def+ handle ((Catch whatMay varNameMay catchS):catches) e = do+ let what = runtimeErrorWhat e+ if whatMay == Just what || whatMay == Nothing+ then+ withLocalScope $ do+ case varNameMay of+ Nothing -> return ()+ Just varName -> setVar varName (toGVal e)+ runStatement catchS+ else+ handle catches e+ -- | Deeply magical function that converts a 'Macro' into a Function. macroToGVal :: forall m h . ( ToGVal (Run m h) h@@ -521,14 +549,14 @@ ] args in case extracted of- Left _ -> fail "Invalid arguments to 'dictsort'"+ Left _ -> throwError $ ArgumentsError "eval" "expected: (src, context)" Right [gSrc, gContext] -> do result <- parseGinger (Prelude.const . return $ Nothing) -- include resolver Nothing -- source name (Text.unpack . asText $ gSrc) -- source code tpl <- case result of- Left err -> fail $ "Error in evaluated code: " ++ show err+ Left err -> throwError $ EvalParseError err Right t -> return t let localLookup varName = return $ lookupLooseDef def (toGVal varName) gContext
src/Text/Ginger/Run/Builtins.hs view
@@ -51,6 +51,7 @@ import Control.Monad.Writer import Control.Monad.Reader import Control.Monad.State+import Control.Monad.Except (throwError) import Control.Applicative import qualified Data.HashMap.Strict as HashMap import Data.HashMap.Strict (HashMap)@@ -241,7 +242,7 @@ Nothing -> return . toGVal . Text.pack $ slice (Text.unpack $ asText slicee) startInt lengthInt- _ -> fail "Invalid arguments to 'slice'"+ _ -> throwError $ ArgumentsError "slice" "expected: (slicee, start=0, length=null)" gfnReplace :: Monad m => Function (Run m h) gfnReplace args =@@ -258,7 +259,7 @@ search = asText searchG replace = asText replaceG return . toGVal $ Text.replace search replace str- _ -> fail "Invalid arguments to 'replace'"+ _ -> throwError $ ArgumentsError "replace" "expected: (str, search, replace)" gfnMap :: Monad m => Function (Run m h) gfnMap args = do@@ -307,8 +308,7 @@ , sortKey , asBoolean reverseG )- _ ->- fail "Invalid args to sort()"+ _ -> throwError $ ArgumentsError "sort" "expected: (sortee, by, reverse)" let -- extractByFunc :: Maybe ((Text, GVal (Run m h)) -> Run m h (GVal (Run m h))) extractByFunc = do f <- asFunction sortKey@@ -554,7 +554,7 @@ return . toGVal $ formatTime locale fmt . convertTZ tzMay <$> dateMay Nothing -> do return . toGVal $ convertTZ tzMay <$> dateMay- _ -> fail "Invalid arguments to 'date'"+ _ -> throwError $ ArgumentsError "date" "expected: (date, format, tz=null, locale=null)" where convertTZ :: Maybe TimeZone -> ZonedTime -> ZonedTime convertTZ Nothing = id@@ -576,7 +576,7 @@ gfnFilter [] = return def gfnFilter [(_, xs)] = return xs gfnFilter ((_, xs):(_, p):args) = do- pfnG <- maybe (fail "Not a function") return (asFunction p)+ pfnG <- maybe (throwError NotAFunctionError) return (asFunction p) let pfn x = asBoolean <$> pfnG ((Nothing, x):args) xsl = fromMaybe [] (asList xs) filtered <- filterM pfn xsl@@ -602,10 +602,14 @@ "key" -> return True "value" -> return False "val" -> return False- x -> fail $ "Invalid value for 'dictsort()' argument 'by': " ++ show x+ x -> throwError $ ArgumentsError "dictsort"+ ( "argument 'by' must be one of 'key', 'value', 'val', " <>+ "but found '" <>+ x <> "'"+ ) let items = fromMaybe [] $ asDictItems gDict let projection = (if caseSensitive then id else Text.toUpper) . (if sortByKey then fst else (asText . snd)) return . orderedDict . List.sortOn projection $ items- _ -> fail "Invalid arguments to 'dictsort'"+ _ -> throwError $ ArgumentsError "dictsort" "expected: (dict, case_sensitive=false, by=null)"
src/Text/Ginger/Run/Type.hs view
@@ -21,6 +21,8 @@ , liftRun2 , Run (..) , RunState (..)+, RuntimeError (..)+, runtimeErrorWhat -- * The Newlines type -- | Required for handling indentation , Newlines (..)@@ -58,9 +60,12 @@ import Text.Ginger.AST import Text.Ginger.Html import Text.Ginger.GVal+import Text.Ginger.Parse (ParserError (..)) import Text.Printf import Text.PrintfA import Data.Scientific (formatScientific)+import Control.Monad.Except (ExceptT (..))+import Data.Default (Default (..), def) import Data.Char (isSpace) import Data.Text (Text)@@ -72,6 +77,7 @@ import Control.Monad.Writer import Control.Monad.Reader import Control.Monad.State+import Control.Monad.Except import Control.Applicative import qualified Data.HashMap.Strict as HashMap import Data.HashMap.Strict (HashMap)@@ -303,12 +309,69 @@ , rsAtLineStart = rsAtLineStart rs } +data RuntimeError = RuntimeError Text -- ^ Generic runtime error+ | UndefinedBlockError Text -- ^ Tried to use a block that isn't defined+ | ArgumentsError -- ^ Invalid arguments to function+ Text -- ^ name of function being called+ Text -- ^ explanation+ | EvalParseError ParserError+ | NotAFunctionError+ deriving (Show)++instance Default RuntimeError where+ def = RuntimeError ""++instance ToGVal m RuntimeError where+ toGVal = runtimeErrorToGVal++runtimeErrorWhat :: RuntimeError -> Text+runtimeErrorWhat (ArgumentsError funcName explanation) = "ArgumentsError"+runtimeErrorWhat (EvalParseError e) = "EvalParseError"+runtimeErrorWhat (RuntimeError msg) = "RuntimeError"+runtimeErrorWhat (UndefinedBlockError blockName) = "UndefinedBlockError"+runtimeErrorWhat NotAFunctionError = "NotAFunctionError"++runtimeErrorToGVal :: RuntimeError -> GVal m+runtimeErrorToGVal (RuntimeError msg) =+ rteGVal "RuntimeError"+ msg []+runtimeErrorToGVal (UndefinedBlockError blockName) =+ rteGVal "UndefinedBlockError"+ ("undefined block: '" <> blockName <> "'")+ [ "block" ~> blockName+ ]+runtimeErrorToGVal (ArgumentsError funcName explanation) =+ rteGVal "ArgumentsError"+ ("invalid arguments to function '" <> funcName <> "': " <> explanation)+ [ "explanation" ~> explanation+ , "function" ~> funcName+ ]+runtimeErrorToGVal (EvalParseError e) =+ rteGVal "EvalParseError"+ ("error parsing eval()-ed code: " <> Text.pack (peErrorMessage e))+ [ "errorMessage" ~> peErrorMessage e+ , "sourceFile" ~> peSourceName e+ , "line" ~> peSourceLine e+ , "col" ~> peSourceColumn e+ ]+runtimeErrorToGVal NotAFunctionError =+ rteGVal "NotAFunctionError"+ ("attempted to call something that is not a function")+ []++rteGVal :: Text -> Text -> [(Text, GVal m)] -> GVal m+rteGVal what msg extra =+ (dict $+ [ "what" ~> what+ , "message" ~> msg+ ] ++ extra) { asText = msg }+ -- | Internal type alias for our template-runner monad stack.-type Run m h = StateT (RunState m h) (ReaderT (GingerContext m h) m)+type Run m h = ExceptT RuntimeError (StateT (RunState m h) (ReaderT (GingerContext m h) m)) -- | Lift a value from the host monad @m@ into the 'Run' monad. liftRun :: Monad m => m a -> Run m h a-liftRun = lift . lift+liftRun = lift . lift . lift -- | Lift a function from the host monad @m@ into the 'Run' monad. liftRun2 :: Monad m => (a -> m b) -> a -> Run m h b@@ -324,7 +387,7 @@ let contextH = hoistContext rev fwd contextT stateT <- get let stateH = hoistRunState rev fwd stateT- (x, stateH') <- lift . lift $ runReaderT (runStateT action stateH) contextH+ (x, stateH') <- lift . lift . lift $ runReaderT (runStateT (runExceptT action) stateH) contextH let stateT' = hoistRunState fwd rev stateH' put stateT'- return x+ Prelude.either throwError return x
test/Text/Ginger/SimulationTests.hs view
@@ -171,6 +171,43 @@ , testCase "if false then \"yes\" else if true then \"maybe\" else \"no\"" $ do mkTestHtml [] [] "{% if false %}yes{% elif true %}maybe{% else %}no{% endif %}" "maybe" ]+ , testGroup "Exceptions"+ [ testCase "try/catch, no exception" $ do+ mkTestHtml+ [] []+ "{% try %}Hello{% catch %}World{% endtry %}"+ "Hello"+ , testCase "try/finally, no exception" $ do+ mkTestHtml+ [] []+ "{% try %}Hello{% finally %} world{% endtry %}"+ "Hello world"+ , testCase "try/catch, trigger arguments exception" $ do+ mkTestHtml+ [] []+ "{% try %}{{ dictsort(1, 2, 3, 4, 5) }}{% catch * as exception %}Caught: {{ exception.what }}{% endtry %}"+ "Caught: ArgumentsError"+ , testCase "try/finally, trigger arguments exception" $ do+ mkTestHtml+ [] []+ "{% try %}{{ dictsort(1, 2, 3, 4, 5) }} Nope!{% finally %}All clear{% endtry %}"+ "All clear"+ , testCase "try/catch, catch selectively (string syntax)" $ do+ mkTestHtml+ [] []+ "{% try %}{{ dictsort(1, 2, 3, 4, 5) }}{% catch * as exception %}Caught: {{ exception.what }}{% endtry %}"+ "Caught: ArgumentsError"+ , testCase "try/catch, catch selectively (identifier syntax)" $ do+ mkTestHtml+ [] []+ "{% try %}{{ dictsort(1, 2, 3, 4, 5) }}{% catch 'ArgumentsError' as exception %}Caught: {{ exception.what }}{% endtry %}"+ "Caught: ArgumentsError"+ , testCase "try/catch, skip non-matching catches" $ do+ mkTestHtml+ [] []+ "{% try %}{{ dictsort(1, 2, 3, 4, 5) }}{% catch 'SomeOtherError' as exception %}This is wrong{% catch * as exception %}Caught: {{ exception.what }}{% endtry %}"+ "Caught: ArgumentsError"+ ] , testGroup "Switch" [ testCase "switch 1 of 1, 2, default" $ do mkTestHtml [] []