microstache 1 → 1.0.1
raw patch · 9 files changed
+126/−68 lines, 9 filesdep ~transformers
Dependency ranges changed: transformers
Files
- CHANGELOG.md +4/−0
- microstache.cabal +2/−1
- mustache-spec/Spec.hs +4/−3
- src/Text/Microstache.hs +6/−1
- src/Text/Microstache/Compile.hs +2/−2
- src/Text/Microstache/Parser.hs +1/−1
- src/Text/Microstache/Render.hs +50/−35
- src/Text/Microstache/Type.hs +35/−9
- tests/Text/Microstache/RenderSpec.hs +22/−16
CHANGELOG.md view
@@ -1,3 +1,7 @@+## microstache 1.0.1++- Add `renderMustacheW`+ ## microstache 1 Initial release, corresponding to `stache-0.2.2`.
microstache.cabal view
@@ -1,5 +1,5 @@ name: microstache-version: 1+version: 1.0.1 cabal-version: >= 1.10 license: BSD3 license-file: LICENSE@@ -65,6 +65,7 @@ Text.Microstache.Render Text.Microstache.Type hs-source-dirs: src+ ghc-options: -Wall default-language: Haskell2010 test-suite tests
mustache-spec/Spec.hs view
@@ -71,7 +71,7 @@ return (specData' bytes) where specData' bytes = describe aspect $ do- let handleError = expectationFailure . show + let handleError = expectationFailure . show case eitherDecode bytes of Left err -> it "should load YAML specs first" $@@ -88,5 +88,6 @@ Left perr -> handleError perr >> undefined Right ns -> return (pname, ns) let ps2 = M.fromList ps1 `M.union` templateCache- renderMustache (Template templateActual ps2) testData- `shouldBe` testExpected+ let (_ws, t) = renderMustacheW (Template templateActual ps2) testData+ -- _ <- traverse print _ws+ t `shouldBe` testExpected
src/Text/Microstache.hs view
@@ -78,12 +78,17 @@ , Key (..) , PName (..) , MustacheException (..)+ , displayMustacheException+ , MustacheWarning (..)+ , displayMustacheWarning -- * Compiling , compileMustacheDir , compileMustacheFile , compileMustacheText -- * Rendering- , renderMustache )+ , renderMustache+ , renderMustacheW+ ) where import Text.Microstache.Compile
src/Text/Microstache/Compile.hs view
@@ -115,8 +115,8 @@ withException = either (throwIO . MustacheParserException) return makeAbsolute' :: FilePath -> IO FilePath-makeAbsolute' path =- fmap (matchTrailingSeparator path . F.normalise) (prependCurrentDirectory path)+makeAbsolute' path0 =+ fmap (matchTrailingSeparator path0 . F.normalise) (prependCurrentDirectory path0) where prependCurrentDirectory :: FilePath -> IO FilePath prependCurrentDirectory path =
src/Text/Microstache/Parser.hs view
@@ -23,7 +23,7 @@ import Data.Maybe (catMaybes) import Data.Text.Lazy (Text) import Text.Parsec hiding ((<|>))-import Text.Parsec.Char+import Text.Parsec.Char () import Data.Word (Word) import Text.Microstache.Type import qualified Data.Text as T
src/Text/Microstache/Render.hs view
@@ -11,19 +11,17 @@ -- import the module, because "Text.Microstache" re-exports everything you may -- need, import that module instead. -{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}-+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-} module Text.Microstache.Render- ( renderMustache )+ ( renderMustache, renderMustacheW ) where -import Control.Exception (throw) import Control.Monad (when, forM_, unless)-import Control.Monad.Trans.Reader-import Control.Monad.Trans.Writer.Lazy+import Control.Monad.Trans.Reader (ReaderT (..), asks, local)+import Control.Monad.Trans.State.Strict (State, modify', execState) import Control.Monad.Trans.Class (lift)-import Data.Aeson+import Data.Aeson (Value (..), encode) import Data.Monoid (mempty) import Data.Semigroup ((<>)) import Data.Foldable (asum)@@ -32,13 +30,10 @@ import Data.Text (Text) import Data.Word (Word) import Text.Microstache.Type-import qualified Data.ByteString.Lazy as B import qualified Data.HashMap.Strict as H import qualified Data.List.NonEmpty as NE import qualified Data.Map as M-import qualified Data.Semigroup as S import qualified Data.Text as T-import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text.Lazy.Builder as B@@ -55,12 +50,21 @@ -- and accumulate the result as 'B.Builder' data which is then turned into -- lazy 'TL.Text'. -type Render a = ReaderT RenderContext (Writer B.Builder) a+type Render a = ReaderT RenderContext (State S) a --- | The render monad context.+data S = S ([MustacheWarning] -> [MustacheWarning]) B.Builder +tellWarning :: MustacheWarning -> Render ()+tellWarning w = lift (modify' f) where+ f (S ws b) = S (ws . (w:)) b++tellBuilder :: B.Builder -> Render ()+tellBuilder b' = lift (modify' f) where+ f (S ws b) = S ws (b <> b')++-- | The render monad context. data RenderContext = RenderContext- { rcIndent :: Maybe Word -- ^ Actual indentation level+ { rcIndent :: Maybe Word -- ^ Actual indentation level , rcContext :: NonEmpty Value -- ^ The context stack , rcPrefix :: Key -- ^ Prefix accumulated by entering sections , rcTemplate :: Template -- ^ The template to render@@ -72,14 +76,14 @@ -- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values -- for interpolation.------ As of version 0.2.0, if referenced values are missing (which almost--- always indicates some sort of mistake), 'MustacheRenderException' will be--- thrown. The included 'Key' will indicate full path to missing value and--- 'PName' will contain the name of active partial.- renderMustache :: Template -> Value -> TL.Text-renderMustache t =+renderMustache t = snd . renderMustacheW t++-- | Like 'renderMustache' but also return a list of warnings.+--+-- @since 1.0.1+renderMustacheW :: Template -> Value -> ([MustacheWarning], TL.Text)+renderMustacheW t = runRender (renderPartial (templateActual t) Nothing renderNode) t -- | Render a single 'Node'.@@ -87,9 +91,9 @@ renderNode :: Node -> Render () renderNode (TextBlock txt) = outputIndented txt renderNode (EscapedVar k) =- lookupKey k >>= outputRaw . escapeHtml . renderValue+ lookupKey k >>= renderValue k >>= outputRaw . escapeHtml renderNode (UnescapedVar k) =- lookupKey k >>= outputRaw . renderValue+ lookupKey k >>= renderValue k >>= outputRaw renderNode (Section k ns) = do val <- lookupKey k enterSection k $@@ -113,21 +117,23 @@ -- | Run 'Render' monad given template to render and a 'Value' to take -- values from. -runRender :: Render a -> Template -> Value -> TL.Text-runRender m t v = (B.toLazyText . execWriter) (runReaderT m rc)+runRender :: Render a -> Template -> Value -> ([MustacheWarning], TL.Text)+runRender m t v = case execState (runReaderT m rc) (S id mempty) of+ S ws b -> (ws [], B.toLazyText b) where rc = RenderContext { rcIndent = Nothing , rcContext = v :| [] , rcPrefix = mempty , rcTemplate = t- , rcLastNode = True }+ , rcLastNode = True+ } {-# INLINE runRender #-} -- | Output a piece of strict 'Text'. outputRaw :: Text -> Render ()-outputRaw = lift . tell . B.fromText+outputRaw = tellBuilder . B.fromText {-# INLINE outputRaw #-} -- | Output indentation consisting of appropriate number of spaces.@@ -194,12 +200,12 @@ lookupKey k = do v <- asks rcContext p <- asks rcPrefix- pname <- asks (templateActual . rcTemplate) let f x = asum (simpleLookup False (x <> k) <$> v) case asum (fmap (f . Key) . reverse . tails $ unKey p) of- -- Context Misses: Failed context lookups should be considered falsey.- -- throw (MustacheRenderException pname (p <> k))- Nothing -> return (String "")+ Nothing -> do+ -- Context Misses: Failed context lookups should be considered falsey.+ tellWarning $ MustacheVariableNotFound (p <> k)+ return (String "") Just r -> return r -- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as@@ -269,10 +275,19 @@ -- | Render Aeson's 'Value' /without/ HTML escaping. -renderValue :: Value -> Text-renderValue Null = ""-renderValue (String str) = str-renderValue value = (TL.toStrict . TL.decodeUtf8 . encode) value+renderValue :: Key -> Value -> Render Text+renderValue k v = case v of+ Null -> return ""+ String str -> return str+ Object _ -> do+ tellWarning (MustacheDirectlyRenderedValue k)+ render v+ Array _ -> do+ tellWarning (MustacheDirectlyRenderedValue k)+ render v+ _ -> render v+ where+ render = return . TL.toStrict . TL.decodeUtf8 . encode {-# INLINE renderValue #-} -- | Escape HTML represented as strict 'Text'.
src/Text/Microstache/Type.hs view
@@ -23,7 +23,11 @@ , Key (..) , showKey , PName (..)- , MustacheException (..) )+ , MustacheException (..)+ , displayMustacheException+ , MustacheWarning (..)+ , displayMustacheWarning+ ) where import Data.Word (Word)@@ -109,20 +113,42 @@ data MustacheException = MustacheParserException ParseError -- ^ Template parser has failed. This contains the parse error.- --- -- /Before version 0.2.0 it was called 'MustacheException'./ | MustacheRenderException PName Key -- ^ A referenced value was not provided. The exception provides info -- about partial in which the issue happened 'PName' and name of the -- missing key 'Key'. deriving (Eq, Show, Typeable, Generic) -#if MIN_VERSION_base(4,8,0)-instance Exception MustacheException where- displayException (MustacheParserException e) = show e- displayException (MustacheRenderException pname key) =+{-# DEPRECATED MustacheRenderException "Not thrown anymore, will be removed in the next major version of microstache" #-}++-- | @since 1.0.1+displayMustacheException :: MustacheException -> String+displayMustacheException (MustacheParserException e) = show e+displayMustacheException (MustacheRenderException pname key) = "Referenced value was not provided in partial \"" ++ T.unpack (unPName pname) ++ "\", key: " ++ T.unpack (showKey key)-#else-instance Exception MustacheException++instance Exception MustacheException where+#if MIN_VERSION_base(4,8,0)+ displayException = displayMustacheException+#endif++-- | @since 1.0.1+data MustacheWarning+ = MustacheVariableNotFound Key+ -- ^ The template contained a variable for which there was no data counterpart in the current context+ | MustacheDirectlyRenderedValue Key+ -- ^ A complex value such as an Object or Array was directly rendered into the template+ deriving (Eq, Show, Typeable, Generic)++-- | @since 1.0.1+displayMustacheWarning :: MustacheWarning -> String+displayMustacheWarning (MustacheVariableNotFound key) = + "Referenced value was not provided, key: " ++ T.unpack (showKey key)+displayMustacheWarning (MustacheDirectlyRenderedValue key) = + "Complex value rendered as such, key: " ++ T.unpack (showKey key)++instance Exception MustacheWarning where+#if MIN_VERSION_base(4,8,0)+ displayException = displayMustacheWarning #endif
tests/Text/Microstache/RenderSpec.hs view
@@ -24,10 +24,14 @@ spec :: Spec spec = describe "renderMustache" $ do- let r ns value =+ let w ns value = let template = Template "test" (M.singleton "test" ns)+ in fst (renderMustacheW template value)+ r ns value = + let template = Template "test" (M.singleton "test" ns) in renderMustache template value- key = Key . pure+ key = Key . return+ it "leaves text block “as is”" $ r [TextBlock "a text block"] Null `shouldBe` "a text block" it "renders escaped variables correctly" $@@ -38,19 +42,23 @@ r [UnescapedVar (key "foo")] (object ["foo" .= ("<html>&\"something\"</html>" :: Text)]) `shouldBe` "<html>&\"something\"</html>"+ context "when rendering a variable" $ do+ it "warns when variable doesn't exist" $+ w [EscapedVar (key "foo")] (object []) `shouldBe`+ [MustacheVariableNotFound (key "foo")]+ it "warns when variable is non-scalar" $+ w [EscapedVar (key "foo")] (object [ "foo" .= object []]) `shouldBe`+ [MustacheDirectlyRenderedValue (key "foo")] context "when rendering a section" $ do let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]]- {-- Not correct according to spec context "when the key is not present" $- it "throws the correct exception" $- evaluate (r nodes (object [])) `shouldThrow`- (== MustacheRenderException "test" (key "foo"))+ it "warns with the correct warning" $+ w nodes (object []) `shouldBe`+ [MustacheVariableNotFound (key "foo")] context "when the key is not present inside a section" $- it "throws the correct exception" $- evaluate (r nodes (object ["foo" .= ([1] :: [Int])])) `shouldThrow`- (== MustacheRenderException "test" (Key ["foo","bar"]))- -}+ it "warns with the correct warning" $+ w nodes (object ["foo" .= ([1] :: [Int])]) `shouldBe`+ [MustacheVariableNotFound (Key ["foo","bar"])] context "when the key is present" $ do context "when the key is a “false” value" $ do it "skips the Null value" $@@ -105,12 +113,10 @@ `shouldBe` "x" context "when rendering an inverted section" $ do let nodes = [InvertedSection (key "foo") [TextBlock "Here!"]]- {- context "when the key is not present" $- it "throws the correct exception" $- evaluate (r nodes (object [])) `shouldThrow`- (== MustacheRenderException "test" (key "foo"))- -}+ it "warns the correct warning" $+ w nodes (object []) `shouldBe`+ [MustacheVariableNotFound (key "foo")] context "when the key is present" $ do context "when the key is a “false” value" $ do it "renders with Null value" $