diff --git a/larceny.cabal b/larceny.cabal index 9686321..48679d2 100644 --- a/larceny.cabal +++ b/larceny.cabal @@ -23,6 +23,7 @@ library , Web.Larceny.Internal , Web.Larceny.Types , Web.Larceny.Fills + , Web.Larceny.Legacy other-extensions: OverloadedStrings build-depends: base >=4.8 && <5 , containers >=0.5 && <0.6 diff --git a/src/Web/Larceny.hs b/src/Web/Larceny.hs index 9742f4d..1ea9b6d 100644 --- a/src/Web/Larceny.hs +++ b/src/Web/Larceny.hs @@ -81,7 +81,8 @@ module Web.Larceny ( Blank(..) , a , (%) , parse - , parseWithOverrides) where + , parseWithOverrides + , LarcenyState(..)) where import Control.Monad (filterM) import Control.Monad.State (evalStateT) @@ -130,7 +131,9 @@ renderWith l sub s = renderRelative l sub s [] renderRelative :: Library s -> Substitutions s -> s -> Path -> Path -> IO (Maybe Text) renderRelative l sub s givenPath targetPath = case findTemplate l givenPath targetPath of - (pth, Just (Template run)) -> Just <$> evalStateT (run pth sub l) s + (pth, Just (Template run)) -> + let larcenyState = LarcenyState pth sub l defaultOverrides print s in + Just <$> evalStateT (run sub) larcenyState (_, Nothing) -> return Nothing -- | Load all the templates in some directory into a Library. diff --git a/src/Web/Larceny/Fills.hs b/src/Web/Larceny/Fills.hs index 107339b..00f42b8 100644 --- a/src/Web/Larceny/Fills.hs +++ b/src/Web/Larceny/Fills.hs @@ -17,7 +17,8 @@ module Web.Larceny.Fills ( textFill , (%)) where import Control.Exception -import Control.Monad.State (StateT) +import Control.Monad.State (StateT, get) +import Control.Monad.Trans (liftIO) import qualified Data.Map as M import Data.Maybe (fromMaybe) import Data.Text (Text) @@ -100,8 +101,7 @@ rawTextFill t = rawTextFill' (return t) -- textFill' getTextFromDatabase -- @ textFill' :: StateT s IO Text -> Fill s -textFill' t = Fill $ \_m _t _l -> HE.text <$> t - +textFill' t = Fill $ \_a _t -> HE.text <$> toLarcenyState t -- | Use state or IO, then fill in some text. -- -- @ @@ -109,7 +109,7 @@ textFill' t = Fill $ \_m _t _l -> HE.text <$> t -- textFill' getTextFromDatabase -- @ rawTextFill' :: StateT s IO Text -> Fill s -rawTextFill' t = Fill $ \_m _t _l -> t +rawTextFill' t = Fill $ \_a _t -> toLarcenyState t -- | Create substitutions for each element in a list and fill the child nodes -- with those substitutions. @@ -124,17 +124,17 @@ rawTextFill' t = Fill $ \_m _t _l -> t mapSubs :: (a -> Substitutions s) -> [a] -> Fill s -mapSubs f xs = Fill $ \_attrs (pth, tpl) lib -> - T.concat <$> mapM (\n -> runTemplate tpl pth (f n) lib) xs +mapSubs f xs = Fill $ \_attrs tpl -> do + T.concat <$> mapM (\n -> runTemplate tpl (f n)) xs -- | Create substitutions for each element in a list (using IO/state if -- needed) and fill the child nodes with those substitutions. mapSubs' :: (a -> StateT s IO (Substitutions s)) -> [a] -> Fill s mapSubs' f xs = Fill $ - \_m (pth, tpl) lib -> + \_m tpl -> do T.concat <$> mapM (\x -> do - s' <- f x - runTemplate tpl pth s' lib) xs + s' <- toLarcenyState $ f x + runTemplate tpl s') xs -- | Fill in the child nodes of the blank with substitutions already -- available. @@ -182,8 +182,8 @@ fillChildrenWith' m = maybeFillChildrenWith' (Just <$> m) -- > Bonnie Thunders maybeFillChildrenWith :: Maybe (Substitutions s) -> Fill s maybeFillChildrenWith Nothing = textFill "" -maybeFillChildrenWith (Just s) = Fill $ \_s (pth, Template tpl) l -> - tpl pth s l +maybeFillChildrenWith (Just s) = Fill $ \_attrs tpl -> do + runTemplate tpl s -- | Use state and IO and maybe fill in with some substitutions. -- @@ -198,11 +198,11 @@ maybeFillChildrenWith (Just s) = Fill $ \_s (pth, Template tpl) l -> -- -- > Bonnie Thunders maybeFillChildrenWith' :: StateT s IO (Maybe (Substitutions s)) -> Fill s -maybeFillChildrenWith' sMSubs = Fill $ \_s (pth, Template tpl) l -> do - mSubs <- sMSubs +maybeFillChildrenWith' sMSubs = Fill $ \_s (Template tpl) -> do + mSubs <- toLarcenyState sMSubs case mSubs of Nothing -> return "" - Just s -> tpl pth s l + Just s -> tpl s -- | Use attributes from the the blank as arguments to the fill. -- @@ -223,8 +223,8 @@ maybeFillChildrenWith' sMSubs = Fill $ \_s (pth, Template tpl) l -> do useAttrs :: (Attributes -> k -> Fill s) -> k -> Fill s -useAttrs k fill= Fill $ \atrs (pth, tpl) lib -> - unFill (k atrs fill) atrs (pth, tpl) lib +useAttrs k fill= Fill $ \atrs tpl -> + unFill (k atrs fill) atrs tpl -- | Prepend `a` to the name of an attribute to pass the value of that -- attribute to the fill. diff --git a/src/Web/Larceny/Internal.hs b/src/Web/Larceny/Internal.hs index f71cbe5..b0fe57c 100644 --- a/src/Web/Larceny/Internal.hs +++ b/src/Web/Larceny/Internal.hs @@ -1,13 +1,13 @@ -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings #-} module Web.Larceny.Internal ( findTemplate , parse , parseWithOverrides) where import Control.Exception -import Lens.Micro +import Lens.Micro ((.~)) +import Control.Monad.State (MonadState, modify, get) import Control.Monad.Trans (liftIO) -import Control.Monad.State (MonadState, StateT, evalStateT, runStateT, get, modify) import qualified Data.HashSet as HS import qualified Data.Map as M import Data.Maybe (fromMaybe) @@ -18,27 +18,17 @@ import qualified Data.Text.Lazy as LT import qualified Text.HTML.DOM as D import qualified Text.XML as X ------------ -import Web.Larceny.Types import Web.Larceny.Fills import Web.Larceny.Html (html5Nodes, html5SelfClosingNodes) import Web.Larceny.Svg (svgNodes) - --- | Turn lazy text into templates. -parse :: LT.Text -> Template s -parse = parseWithOverrides defaultOverrides - --- | Use overrides when parsing a template. -parseWithOverrides :: Overrides -> LT.Text -> Template s -parseWithOverrides o t = - let textWithoutDoctype = LT.replace "" "" t - (X.Document _ (X.Element _ _ nodes) _) = D.parseLT ("
" <> textWithoutDoctype <> "
") - in mk o $! map (toLarcenyNode o) nodes +import Web.Larceny.Types -- | Phases of the template parsing/rendering process: 1. Parse the document -- into HTML (or really, XML) nodes 2. Turn those nodes into Larceny nodes, -- which encodes more information about the elements, including prefix and -- whether the node is a regular HTML node, a special Larceny element, or a -- Larceny blank. 3. Render each node into Text according to its node type. + data Node = NodeElement Element | NodeContent Text | NodeComment Text @@ -49,6 +39,18 @@ data Element = PlainElement Name Attributes [Node] | BlankElement Name Attributes [Node] | DoctypeElement +-- | Turn lazy text into templates. +parse :: LT.Text -> Template s +parse = parseWithOverrides defaultOverrides + +-- | Use overrides when parsing a template. +parseWithOverrides :: Overrides -> LT.Text -> Template s +parseWithOverrides o t = + let textWithoutDoctype = LT.replace "" "" t + (X.Document _ (X.Element _ _ nodes) _) = D.parseLT ("
" <> textWithoutDoctype <> "
") + in mk $! map (toLarcenyNode o) nodes + + toLarcenyName :: X.Name -> Name toLarcenyName (X.Name tn _ _) = case T.stripPrefix "l:" tn of @@ -93,25 +95,13 @@ toLarcenyNode _ (X.NodeComment c) = NodeComment c toLarcenyNode _ (X.NodeInstruction _) = NodeContent "" -- | Turn HTML nodes and overrides into templates. -mk :: Overrides -> [Node] -> Template s -mk o = f +mk :: [Node] -> Template s +mk = f where f nodes = - Template $ \pth m l -> - let pc = ProcessContext pth m l o f nodes in - do s <- get - T.concat <$> toUserState (pc s) (process nodes) - -toProcessState :: StateT s IO a -> StateT (ProcessContext s) IO a -toProcessState f = - do pc <- get - (result, s') <- liftIO $ runStateT f (_pcState pc) - pcState .= s' - return result - -toUserState :: ProcessContext s -> StateT (ProcessContext s) IO a -> StateT s IO a -toUserState pc f = - do s <- get - liftIO $ evalStateT f (pc { _pcState = s }) + Template $ \m -> + do modify (lSubs .~ m) + -- lPath .= pth + T.concat <$> process nodes fillIn :: Blank -> Substitutions s -> Fill s fillIn tn m = fromMaybe (fallbackFill tn m) (M.lookup tn m) @@ -120,45 +110,21 @@ fallbackFill :: Blank -> Substitutions s -> Fill s fallbackFill FallbackBlank m = fromMaybe (textFill "") (M.lookup FallbackBlank m) fallbackFill (Blank tn) m = let fallback = fromMaybe (textFill "") (M.lookup FallbackBlank m) in - Fill $ \attr (pth, tpl) lib -> - do liftIO $ putStrLn ("Larceny: Missing fill for blank " <> show tn <> " in template " <> show pth) - unFill fallback attr (pth, tpl) lib - -data ProcessContext s = ProcessContext { _pcPath :: Path - , _pcSubs :: Substitutions s - , _pcLib :: Library s - , _pcOverrides :: Overrides - , _pcMk :: [Node] -> Template s - , _pcNodes :: [Node] - , _pcState :: s } - -infix 4 .= -(.=) :: MonadState s m => ASetter s s a b -> b -> m () -l .= b = modify (l .~ b) -{-# INLINE (.=) #-} - -pcSubs :: Lens' (ProcessContext s) (Substitutions s) -pcSubs = lens _pcSubs (\pc s -> pc { _pcSubs = s }) - -pcNodes :: Lens' (ProcessContext s) [Node] -pcNodes = lens _pcNodes (\pc n -> pc { _pcNodes = n }) - -pcState :: Lens' (ProcessContext s) s -pcState = lens _pcState (\pc s -> pc { _pcState = s }) - -type ProcessT s = StateT (ProcessContext s) IO [Text] + Fill $ \attr tpl -> + do st <- get + let pth = _lPath st + liftIO $ putStrLn ("Larceny: Missing fill for blank " <> show tn <> " in template " <> show pth) + unFill fallback attr tpl add :: Substitutions s -> Template s -> Template s add mouter tpl = - Template (\pth minner l -> runTemplate tpl pth (minner `M.union` mouter) l) + Template (\minner -> runTemplate tpl (minner `M.union` mouter)) -process :: [Node] -> ProcessT s +process :: [Node] -> LarcenyM s [Text] process [] = return [] process (NodeElement (BindElement atr kids):nextNodes) = do - pcNodes .= nextNodes - processBind atr kids + processBind atr kids nextNodes process (currentNode:nextNodes) = do - pcNodes .= nextNodes processedNode <- case currentNode of NodeElement DoctypeElement -> return [""] @@ -182,12 +148,12 @@ process (currentNode:nextNodes) = do processPlain :: Name -> Attributes -> [Node] -> - ProcessT s + LarcenyM s [Text] processPlain tagName atr kids = do pc <- get atrs <- attrsToText atr processed <- process kids - return $ tagToText (_pcOverrides pc) tagName atrs processed + return $ tagToText (_lOverrides pc) tagName atrs processed selfClosing :: Overrides -> HS.HashSet Text selfClosing (Overrides _ _ sc) = @@ -206,7 +172,7 @@ tagToText overrides (Name mPf name) atrs processed = ++ processed ++ [" prefix <> name <> ">"] -attrsToText :: Attributes -> StateT (ProcessContext s) IO Text +attrsToText :: Attributes -> LarcenyM s Text attrsToText attrs = T.concat <$> mapM attrToText (M.toList attrs) where attrToText (k,v) = do @@ -217,7 +183,7 @@ attrsToText attrs = toText (k, "") = " " <> k toText (k, v) = " " <> k <> "=\"" <> T.strip v <> "\"" -fillAttrs :: Attributes -> StateT (ProcessContext s) IO Attributes +fillAttrs :: Attributes -> LarcenyM s Attributes fillAttrs attrs = M.fromList <$> mapM fill (M.toList attrs) where fill p = do let (unboundKeys, unboundValues) = eUnboundAttrs p @@ -225,12 +191,11 @@ fillAttrs attrs = M.fromList <$> mapM fill (M.toList attrs) vals <- T.concat <$> mapM fillAttr unboundValues return (keys, vals) -fillAttr :: Either Text Blank -> StateT (ProcessContext s) IO Text +fillAttr :: Either Text Blank -> LarcenyM s Text fillAttr eBlankText = - do (ProcessContext pth m l _ mko _ _) <- get - toProcessState $ - case eBlankText of - Right hole -> unFill (fillIn hole m) mempty (pth, mko []) l + do m <- _lSubs <$> get + case eBlankText of + Right hole -> unFill (fillIn hole m) mempty (mk []) Left text -> return text -- Look up the Fill for the hole. Apply the Fill to a map of @@ -239,24 +204,25 @@ fillAttr eBlankText = processBlank :: Text -> Attributes -> [Node] -> - ProcessT s + LarcenyM s [Text] processBlank tagName atr kids = do - (ProcessContext pth m l _ mko _ _) <- get + m <- _lSubs <$> get filled <- fillAttrs atr - sequence [ toProcessState $ unFill (fillIn (Blank tagName) m) + sequence [ unFill (fillIn (Blank tagName) m) filled - (pth, add m (mko kids)) l] + (add m (mk kids))] processBind :: Attributes -> [Node] -> - ProcessT s -processBind atr kids = do - (ProcessContext pth m l _ mko nodes _) <- get + [Node] -> + LarcenyM s [Text] +processBind atr kids nextNodes = do + m <- _lSubs <$> get let tagName = atr M.! "tag" - newSubs = subs [(tagName, Fill $ \_a _t _l -> - runTemplate (mko kids) pth m l)] - pcSubs .= newSubs `M.union` m - process nodes + newSubs = subs [(tagName, Fill $ \_a _t ->do + runTemplate (mk kids) m)] + modify (lSubs .~ newSubs `M.union` m) + process nextNodes -- Look up the template that's supposed to be applied in the library, -- create a substitution for the content hole using the child elements @@ -264,15 +230,16 @@ processBind atr kids = do -- combined with outer substitution and the library. processApply :: Attributes -> [Node] -> - ProcessT s + LarcenyM s [Text] processApply atr kids = do - (ProcessContext pth m l _ mko _ _) <- get + (LarcenyState pth m l _ _ _) <- get filledAttrs <- fillAttrs atr let (absolutePath, tplToApply) = findTemplateFromAttrs pth l filledAttrs - contentTpl <- toProcessState $ runTemplate (mko kids) pth m l + contentTpl <- runTemplate (mk kids) m let contentSub = subs [("apply-content", rawTextFill contentTpl)] - sequence [ toProcessState $ runTemplate tplToApply absolutePath (contentSub `M.union` m) l ] + modify (lPath .~ absolutePath) + sequence [ runTemplate tplToApply (contentSub `M.union` m) ] findTemplateFromAttrs :: Path -> Library s -> diff --git a/src/Web/Larceny/Legacy.hs b/src/Web/Larceny/Legacy.hs new file mode 100644 index 0000000..9700850 --- /dev/null +++ b/src/Web/Larceny/Legacy.hs @@ -0,0 +1,92 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Web.Larceny.Legacy ( textFill + , textFill' + , rawTextFill + , rawTextFill' + , mapSubs + , mapSubs' + , fillChildren + , fillChildrenWith + , fillChildrenWith' + , maybeFillChildrenWith + , maybeFillChildrenWith' + , ifFill + , useAttrs + , a + , (%)) where + +import Control.Monad.State (StateT) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import qualified Data.Text as T +import qualified HTMLEntities.Text as HE +------------ +import Web.Larceny.Fills (a, (%)) +import Web.Larceny.Types + +ifFill :: Fill s +ifFill = + useAttrs (a "condition" % a "exists") ifFill' + where ifFill' :: Maybe Bool -> Maybe Text -> Fill s + ifFill' mCondition mExisting = + let condition = fromMaybe True mCondition + existing = fromMaybe "exist" mExisting + bool = condition && existing /= "" + thenElseSubs = subs [("then", thenFill bool) + ,("else", thenFill (not bool))] in + fillChildrenWith thenElseSubs + thenFill True = fillChildren + thenFill False = textFill "" + +textFill :: Text -> Fill s +textFill t = textFill' (return t) + +rawTextFill :: Text -> Fill s +rawTextFill t = rawTextFill' (return t) + +textFill' :: StateT s IO Text -> Fill s +textFill' t = Fill $ \_a _t -> HE.text <$> toLarcenyState t + +rawTextFill' :: StateT s IO Text -> Fill s +rawTextFill' t = Fill $ \_a _t -> toLarcenyState t + +mapSubs :: (a -> Substitutions s) + -> [a] + -> Fill s +mapSubs f xs = Fill $ \_attrs tpl -> do + T.concat <$> mapM (\n -> runTemplate tpl (f n)) xs + +mapSubs' :: (a -> StateT s IO (Substitutions s)) -> [a] -> Fill s +mapSubs' f xs = Fill $ + \_m tpl -> do + T.concat <$> mapM (\x -> do + s' <- toLarcenyState $ f x + runTemplate tpl s') xs + +fillChildren :: Fill s +fillChildren = fillChildrenWith mempty + +fillChildrenWith :: Substitutions s -> Fill s +fillChildrenWith m = maybeFillChildrenWith (Just m) + +fillChildrenWith' :: StateT s IO (Substitutions s) -> Fill s +fillChildrenWith' m = maybeFillChildrenWith' (Just <$> m) + +maybeFillChildrenWith :: Maybe (Substitutions s) -> Fill s +maybeFillChildrenWith Nothing = textFill "" +maybeFillChildrenWith (Just s) = Fill $ \_attrs tpl -> do + runTemplate tpl s + +maybeFillChildrenWith' :: StateT s IO (Maybe (Substitutions s)) -> Fill s +maybeFillChildrenWith' sMSubs = Fill $ \_s (Template tpl) -> do + mSubs <- toLarcenyState sMSubs + case mSubs of + Nothing -> return "" + Just s -> tpl s + +useAttrs :: (Attributes -> k -> Fill s) + -> k + -> Fill s +useAttrs k fill= Fill $ \atrs tpl -> + unFill (k atrs fill) atrs tpl diff --git a/src/Web/Larceny/Types.hs b/src/Web/Larceny/Types.hs index d9ed1b0..094fc82 100644 --- a/src/Web/Larceny/Types.hs +++ b/src/Web/Larceny/Types.hs @@ -14,10 +14,20 @@ module Web.Larceny.Types ( Blank(..) , defaultOverrides , FromAttribute(..) , AttrError(..) - , ApplyError(..)) where + , ApplyError(..) + , LarcenyState(..) + , LarcenyM + , toLarcenyState + , lSubs + , lPath + , lLib + , lOverrides + , lAppState) where import Control.Exception -import Control.Monad.State (StateT) +import Lens.Micro +import Control.Monad.State (StateT, get, put, runStateT, modify, MonadState) +import Control.Monad.Trans (liftIO) import Data.Hashable (Hashable, hash, hashWithSalt) import Data.Map (Map) import qualified Data.Map as M @@ -26,6 +36,35 @@ import Data.Text (Text) import qualified Data.Text as T import Text.Read (readMaybe) +type LarcenyM s a = StateT (LarcenyState s) IO a + +data LarcenyState s = + LarcenyState { _lPath :: [Text] + , _lSubs :: Substitutions s + , _lLib :: Library s + , _lOverrides :: Overrides + , _lLogger :: (Text -> IO ()) + , _lAppState :: s } + +lSubs :: Lens' (LarcenyState s) (Substitutions s) +lSubs = lens _lSubs (\l s -> l { _lSubs = s }) +lPath :: Lens' (LarcenyState s) Path +lPath = lens _lPath (\l s -> l { _lPath = s }) +lLib :: Lens' (LarcenyState s) (Library s) +lLib = lens _lLib (\l s -> l { _lLib = s }) +lOverrides :: Lens' (LarcenyState s) Overrides +lOverrides = lens _lOverrides (\ls o -> ls { _lOverrides = o }) +lAppState:: Lens' (LarcenyState s) s +lAppState = lens _lAppState (\ls s -> ls { _lAppState = s }) + +-- TODO: This shouldn't be necessary. +toLarcenyState :: StateT s IO a -> LarcenyM s a +toLarcenyState f = do + l <- get + (result, newAppState) <- liftIO $ runStateT f (_lAppState l) + put (l {_lAppState = newAppState }) + return result + -- | Corresponds to a "blank" in the template that can be filled in -- with some value when the template is rendered. Blanks can be tags -- or they can be all or parts of attribute values in tags. @@ -51,8 +90,8 @@ instance Hashable Blank where -- from scratch. -- -- @ --- Fill $ \attrs _tpl _lib -> --- return $ T.pack $ show $ M.keys attrs) +-- Fill $ \attrs _tpl -> +-- return $ Text.intercalate " and " $ M.keys attrs) -- @ -- -- With that Fill, a Blank like this: @@ -61,16 +100,15 @@ instance Hashable Blank where -- -- would be rendered as: -- --- > ["attribute", "another"] +-- > another and attribute -- -- Fills (and Substitutions and Templates) have the type `StateT s IO -- Text` in case you need templates to depend on IO actions (like -- looking something up in a database) or store state (perhaps keeping -- track of what's already been rendered). newtype Fill s = Fill { unFill :: Attributes - -> (Path, Template s) - -> Library s - -> StateT s IO Text } + -> Template s + -> LarcenyM s Text } -- | The Blank's attributes, a map from the attribute name to -- it's value. @@ -116,10 +154,8 @@ fallbackSub fill = M.fromList [(FallbackBlank, fill)] -- Use `loadTemplates` to load the templates from some directory -- into a template library. Use the `render` functions to render -- templates from a Library by path. -newtype Template s = Template { runTemplate :: Path - -> Substitutions s - -> Library s - -> StateT s IO Text } +newtype Template s = Template { runTemplate :: Substitutions s + -> LarcenyM s Text } -- | The path to a template. type Path = [Text] diff --git a/test/Examples.hs b/test/Examples.hs index 0b1c4e0..2dca620 100644 --- a/test/Examples.hs +++ b/test/Examples.hs @@ -64,9 +64,10 @@ subst = subs [ ("site-title", textFill "Gotham Girls roster") modifyInnerText :: (Text -> Text) -> Fill () modifyInnerText f = Fill $ - \_attrs (_pth, tpl) _l -> + \_attrs tpl -> liftIO $ do - t' <- evalStateT (runTemplate tpl ["default"] mempty mempty) () + let larcenyState = LarcenyState ["default"] mempty mempty defaultOverrides print () + t' <- evalStateT (runTemplate tpl mempty) larcenyState return $ f t' tplLib :: Library () diff --git a/test/Spec.hs b/test/Spec.hs index 716bdd1..3dd418b 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,18 +1,18 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE BangPatterns #-} {-# LANGUAGE TypeSynonymInstances #-} import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) -import Control.Exception (Exception, catch, throw, try, ErrorCall(..)) -import Lens.Micro -import Control.Monad.State (StateT (..), evalStateT, get, modify, - runStateT) +import Control.Exception (ErrorCall (..), Exception, catch, + throw, try) +import Control.Monad.State (StateT (..), evalStateT, get, + runStateT, modify, MonadState) import qualified Control.Monad.State as S import Control.Monad.Trans (liftIO) import qualified Data.Map as M @@ -23,43 +23,33 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as LT import Data.Typeable import Examples +import Lens.Micro import Test.Hspec import qualified Test.Hspec.Core.Spec as H -import Web.Larceny - -infix 4 .= -(.=) :: S.MonadState s m => ASetter s s a b -> b -> m () -l .= b = modify (l .~ b) -{-# INLINE (.=) #-} -data LarcenyState = - LarcenyState { _lPath :: [Text] - , _lSubs :: Substitutions () - , _lLib :: Library () - , _lOverrides :: Overrides } -lPath :: Lens' LarcenyState [Text] -lPath = lens _lPath (\ls p -> ls { _lPath = p }) -lSubs :: Lens' LarcenyState (Substitutions ()) -lSubs = lens _lSubs (\ls s -> ls { _lSubs = s }) -lLib :: Lens' LarcenyState (Library ()) -lLib = lens _lLib (\ls l -> ls { _lLib = l }) -lOverrides :: Lens' LarcenyState Overrides -lOverrides = lens _lOverrides (\ls o -> ls { _lOverrides = o }) +import Web.Larceny +import Web.Larceny.Types (lAppState, lLib, lOverrides, lPath, + lSubs) -type LarcenyHspecM = StateT LarcenyHspecState IO +type LarcenyHspecM s = StateT (LarcenyHspecState s) IO -data LarcenyHspecState = +data LarcenyHspecState s = LarcenyHspecState { _hResult :: H.Result - , _hLarcenyState :: LarcenyState } + , _hLarcenyState :: LarcenyState s } + +infix 4 .= +(.=) :: MonadState s m => ASetter s s a b -> b -> m () +l .= b = modify (l .~ b) +{-# INLINE (.=) #-} -hResult :: Lens' LarcenyHspecState H.Result +hResult :: Lens' (LarcenyHspecState s) H.Result hResult = lens _hResult (\hs r -> hs { _hResult = r }) -hLarcenyState :: Lens' LarcenyHspecState LarcenyState +hLarcenyState :: Lens' (LarcenyHspecState s) (LarcenyState s) hLarcenyState = lens _hLarcenyState (\hs ls -> hs { _hLarcenyState = ls }) -instance H.Example (LarcenyHspecM ()) where - type Arg (LarcenyHspecM ()) = LarcenyHspecState +instance H.Example (LarcenyHspecM s ()) where + type Arg (LarcenyHspecM a ()) = LarcenyHspecState a evaluateExample s _params actionWithToIO _progCallback = do mv <- newEmptyMVar actionWithToIO $ \st -> @@ -70,15 +60,16 @@ instance H.Example (LarcenyHspecM ()) where putMVar mv r takeMVar mv -withLarceny :: SpecWith LarcenyHspecState +withLarceny :: s + -> SpecWith (LarcenyHspecState s) -> Spec -withLarceny spec' = +withLarceny s spec' = let larcenyHspecState = - LarcenyHspecState H.Success (LarcenyState ["default"] mempty mempty mempty) in + LarcenyHspecState H.Success (LarcenyState ["default"] mempty mempty mempty print s) in afterAll return $ before (return larcenyHspecState) spec' -setResult :: H.Result -> LarcenyHspecM () +setResult :: H.Result -> LarcenyHspecM s () setResult r = case r of H.Success -> hResult .= r _ -> throw r @@ -111,13 +102,13 @@ tpl4Output = "\ removeSpaces :: Text -> Text removeSpaces = T.replace " " "" -renderM :: Text -> LarcenyHspecM Text +renderM :: Text -> LarcenyHspecM s Text renderM templateText = do - (LarcenyHspecState _ (LarcenyState p s l o)) <- S.get + (LarcenyHspecState _ ls@(LarcenyState _ s _ o _ _)) <- S.get let tpl = parseWithOverrides o (LT.fromStrict templateText) - liftIO $ evalStateT (runTemplate tpl p s l) () + liftIO $ evalStateT (runTemplate tpl s) ls -shouldRenderM :: Text -> Text -> LarcenyHspecM () +shouldRenderM :: Text -> Text -> LarcenyHspecM s () shouldRenderM templateText output = do rendered <- renderM templateText if removeSpaces rendered == removeSpaces output @@ -125,7 +116,7 @@ shouldRenderM templateText output = do else let msg = T.unpack $ rendered <> " doesn't match " <> output in setResult (H.Fail Nothing msg) -shouldRenderContainingM :: Text -> Text -> LarcenyHspecM () +shouldRenderContainingM :: Text -> Text -> LarcenyHspecM s () shouldRenderContainingM templateText excerpt = do rendered <- renderM templateText if excerpt `T.isInfixOf` rendered @@ -133,7 +124,7 @@ shouldRenderContainingM templateText excerpt = do else let msg = T.unpack $ excerpt <> " not found in " <> templateText in setResult (H.Fail Nothing msg) -shouldErrorM :: (Exception a, Eq a) => Text -> Selector a -> LarcenyHspecM () +shouldErrorM :: (Exception a, Eq a) => Text -> Selector a -> LarcenyHspecM s () shouldErrorM templateText p = do hspecState <- S.get let renderAttempt = evalStateT (renderM templateText) hspecState @@ -160,7 +151,7 @@ main = spec spec :: IO () spec = hspec $ do - withLarceny $ do + withLarceny () $ do describe "parse" $ do it "should parse HTML into a Template" $ do hLarcenyState.lSubs .= subst @@ -290,7 +281,7 @@ spec = hspec $ do \" `shouldRenderM` "Fill this in Fill this in" it "should not let binds escape the apply-content tag" $ do - hLarcenyState.lSubs .= fallbackSub (Fill $ \_ _ _ -> error "not found!") + hLarcenyState.lSubs .= fallbackSub (Fill $ \_ _ -> error "not found!") let lib = M.fromList [(["blah"], parse "")] hLarcenyState.lLib .= lib " \ @@ -318,7 +309,7 @@ spec = hspec $ do it "should allow you to write functions for fills" $ do let subs' = subs [("desc", - Fill $ \m _t _l -> return $ T.take (read $ T.unpack (m M.! "length")) + Fill $ \m _t-> return $ T.take (read $ T.unpack (m M.! "length")) "A really long description" <> "...")] hLarcenyState.lSubs .= subs' @@ -327,8 +318,8 @@ spec = hspec $ do it "should allow you to use IO in fills" $ do let subs' = subs [("desc", Fill $ - \m _t _l -> do liftIO $ putStrLn "***********\nHello World\n***********" - return $ T.take (read $ T.unpack (m M.! "length")) + \m _t -> do liftIO $ putStrLn "***********\nHello World\n***********" + return $ T.take (read $ T.unpack (m M.! "length")) "A really long description" <> "...")] hLarcenyState.lSubs .= subs' @@ -376,11 +367,21 @@ spec = hspec $ do `shouldRenderM` "

" it "should know what the template path is" $ do - let fill = Fill $ \_ (p, _) _ -> return (head p) + let fill = Fill $ \_ _ -> do + st <- get + let p = _lPath st + return (head p) hLarcenyState.lSubs .= subs [("template", fill)] "

" `shouldRenderM` "

" + it "should work like the docs say" $ do + let fill = Fill $ \attrs _tpl -> + return $ T.intercalate " and " $ M.keys attrs + hLarcenyState.lSubs .= subs [("displayAttrs", fill)] + "" + `shouldRenderM` "attribute1 and attribute2" + describe "a large template" $ do it "should render large HTML files" $ do hLarcenyState.lSubs .= subst @@ -420,9 +421,9 @@ spec = hspec $ do doctypeTests conditionalTests namespaceTests - statefulTests + withLarceny 0 $ statefulTests -namespaceTests :: SpecWith LarcenyHspecState +namespaceTests :: SpecWith (LarcenyHspecState ()) namespaceTests = describe "namespaces" $ do it "should assume that tags with namespaces are blanks" $ do @@ -441,18 +442,22 @@ namespaceTests = "" `shouldRenderM` "" -statefulTests :: SpecWith () +statefulTests :: SpecWith (LarcenyHspecState Int) statefulTests = + let incrementSub = + subs [("increment-and-print", + Fill $ \_ _ -> + do -- eek refactor later + s <- get + lAppState .= (_lAppState s + 1 :: Int) + s' <- get + let int = _lAppState s' + return (T.pack (show int)))] in describe "statefulness" $ do it "a fill should be able to affect subsequent fills" $ do - renderWith (M.fromList [(["default"], parse "")]) - (subs [("x", Fill $ \_ _ _ -> - do modify ((+1) :: Int -> Int) - s <- get - return (T.pack (show s)))]) - 0 - ["default"] - `shouldReturn` Just "12" + hLarcenyState.lSubs .= incrementSub + "" `shouldRenderM` "12" + {- The following test was prompted by a bug where I refuktored the bind tag handling to be inside the case statement in `process`. The bind tag processor itself calls bind but doesn't return any @@ -460,19 +465,13 @@ statefulTests = over and over again. -} it "should not be affected by binds" $ do - let tpl = "test1\ - \test2\ - \" - renderWith (M.fromList [(["default"], parse tpl)]) - (subs [("x", Fill $ \_ _ _ -> - do modify ((+1) :: Int -> Int) - s <- get - return (T.pack (show s)))]) - 0 - ["default"] - `shouldReturn` Just "12" - -doctypeTests :: SpecWith LarcenyHspecState + hLarcenyState.lSubs .= incrementSub + "test1\ + \test2\ + \" + `shouldRenderM` "12" + +doctypeTests :: SpecWith (LarcenyHspecState ()) doctypeTests = do describe "doctypes" $ do it "should render blank doctypes" $ do @@ -483,7 +482,7 @@ doctypeTests = do "

Hello world

" `shouldRenderM` "

Hello world

" -conditionalTests :: SpecWith LarcenyHspecState +conditionalTests :: SpecWith (LarcenyHspecState ()) conditionalTests = do describe "conditionals" $ do let template cond = @@ -598,7 +597,7 @@ conditionalTests = do template `shouldRenderM` "It is empty!" -fallbackTests ::SpecWith LarcenyHspecState +fallbackTests ::SpecWith (LarcenyHspecState ()) fallbackTests = do describe "templates with missing blanks" $ do it "should render empty text by default" $ do @@ -610,10 +609,10 @@ fallbackTests = do hLarcenyState.lSubs .= fallbackSub (rawTextFill "I'm a fallback.") "

missing:

" `shouldRenderM` "

missing: I'm a fallback.

" it "should allow errors to be thrown, e.g., in dev mode" $ do - hLarcenyState.lSubs .= fallbackSub (Fill $ \_ _ _ -> error "missing blank!") + hLarcenyState.lSubs .= fallbackSub (Fill $ \_ _ -> error "missing blank!") "

missing:

" `shouldErrorM` (== ErrorCall "missing blank!") -attrTests :: SpecWith LarcenyHspecState +attrTests :: SpecWith (LarcenyHspecState ()) attrTests = describe "useAttrs" $ do it "should allow you to *easily* write functions for fills" $ do @@ -634,8 +633,9 @@ attrTests = it "should allow you use child elements" $ do let descTplFill = useAttrs (a"length") - (\n -> Fill $ \_attrs (_pth, tpl) _l -> liftIO $ do - t' <- evalStateT (runTemplate tpl ["default"] mempty mempty) () + (\n -> Fill $ \_attrs tpl -> liftIO $ do + let larcenyState = LarcenyState ["default"] mempty mempty defaultOverrides print () + t' <- evalStateT (runTemplate tpl mempty) larcenyState return $ T.take n t' <> "...") hLarcenyState.lSubs .= subs [ ("adverb", textFill "really") , ("desc", descTplFill)] @@ -674,8 +674,9 @@ attrTests = descFunc :: Int -> Maybe Text -> Fill () descFunc n e = Fill $ do let ending = fromMaybe "..." e - \_attrs (_pth, tpl) _l -> liftIO $ do - renderedText <- evalStateT (runTemplate tpl ["default"] mempty mempty) () + \_attrs tpl -> liftIO $ do + let larcenyState = LarcenyState ["default"] mempty mempty defaultOverrides print () + renderedText <- evalStateT (runTemplate tpl mempty) larcenyState return $ T.take n renderedText <> ending {-# ANN module ("HLint: ignore Redundant do" :: String) #-}