stache 2.3.4 → 2.4.0
raw patch · 25 files changed
+715/−385 lines, 25 filesdep ~bytestringdep ~template-haskellsetup-changedPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: bytestring, template-haskell
API changes (from Hackage documentation)
- Text.Mustache.Type: instance Data.Data.Data Text.Mustache.Type.Key
- Text.Mustache.Type: instance Data.Data.Data Text.Mustache.Type.Node
- Text.Mustache.Type: instance Data.Data.Data Text.Mustache.Type.PName
- Text.Mustache.Type: instance Data.Data.Data Text.Mustache.Type.Template
- Text.Mustache.Type: instance Data.String.IsString Text.Mustache.Type.PName
- Text.Mustache.Type: instance GHC.Base.Monoid Text.Mustache.Type.Key
- Text.Mustache.Type: instance GHC.Base.Semigroup Text.Mustache.Type.Key
- Text.Mustache.Type: instance GHC.Base.Semigroup Text.Mustache.Type.Template
- Text.Mustache.Type: instance GHC.Exception.Type.Exception Text.Mustache.Type.MustacheException
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.Key
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.MustacheException
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.MustacheWarning
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.Node
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.PName
- Text.Mustache.Type: instance GHC.Generics.Generic Text.Mustache.Type.Template
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.Key
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.MustacheException
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.MustacheWarning
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.Node
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.PName
- Text.Mustache.Type: instance GHC.Show.Show Text.Mustache.Type.Template
+ Text.Mustache.Render.Internal: IndentEveryLine :: IndentationPolicy
+ Text.Mustache.Render.Internal: IndentOnlyFirstLine :: IndentationPolicy
+ Text.Mustache.Render.Internal: addContext :: Value -> R a -> R a
+ Text.Mustache.Render.Internal: addPrefix :: Key -> R a -> R a
+ Text.Mustache.Render.Internal: addWarning :: MustacheWarning -> R ()
+ Text.Mustache.Render.Internal: data IndentationPolicy
+ Text.Mustache.Render.Internal: data R a
+ Text.Mustache.Render.Internal: getContext :: R (NonEmpty Value)
+ Text.Mustache.Render.Internal: getPrefix :: R Key
+ Text.Mustache.Render.Internal: incrementIndentBy :: Pos -> R a -> R a
+ Text.Mustache.Render.Internal: instance GHC.Internal.Base.Applicative Text.Mustache.Render.Internal.R
+ Text.Mustache.Render.Internal: instance GHC.Internal.Base.Functor Text.Mustache.Render.Internal.R
+ Text.Mustache.Render.Internal: instance GHC.Internal.Base.Monad Text.Mustache.Render.Internal.R
+ Text.Mustache.Render.Internal: lookupTemplate :: PName -> R (Maybe [Node])
+ Text.Mustache.Render.Internal: resetPrefix :: R a -> R a
+ Text.Mustache.Render.Internal: runR :: R () -> Value -> Map PName [Node] -> ([MustacheWarning], Text)
+ Text.Mustache.Render.Internal: txt :: IndentationPolicy -> Text -> R ()
+ Text.Mustache.Type: instance GHC.Internal.Base.Monoid Text.Mustache.Type.Key
+ Text.Mustache.Type: instance GHC.Internal.Base.Semigroup Text.Mustache.Type.Key
+ Text.Mustache.Type: instance GHC.Internal.Base.Semigroup Text.Mustache.Type.Template
+ Text.Mustache.Type: instance GHC.Internal.Data.Data.Data Text.Mustache.Type.Key
+ Text.Mustache.Type: instance GHC.Internal.Data.Data.Data Text.Mustache.Type.Node
+ Text.Mustache.Type: instance GHC.Internal.Data.Data.Data Text.Mustache.Type.PName
+ Text.Mustache.Type: instance GHC.Internal.Data.Data.Data Text.Mustache.Type.Template
+ Text.Mustache.Type: instance GHC.Internal.Data.String.IsString Text.Mustache.Type.PName
+ Text.Mustache.Type: instance GHC.Internal.Exception.Type.Exception Text.Mustache.Type.MustacheException
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.Key
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.MustacheException
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.MustacheWarning
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.Node
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.PName
+ Text.Mustache.Type: instance GHC.Internal.Generics.Generic Text.Mustache.Type.Template
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.Key
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.MustacheException
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.MustacheWarning
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.Node
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.PName
+ Text.Mustache.Type: instance GHC.Internal.Show.Show Text.Mustache.Type.Template
Files
- CHANGELOG.md +11/−0
- CODE_OF_CONDUCT.md +0/−35
- CONTRIBUITING.md +0/−13
- LICENSE +24/−17
- README.md +3/−9
- Setup.hs +0/−6
- Text/Mustache.hs +1/−1
- Text/Mustache/Compile.hs +5/−5
- Text/Mustache/Compile/TH.hs +3/−3
- Text/Mustache/Parser.hs +3/−3
- Text/Mustache/Render.hs +68/−200
- Text/Mustache/Render/Internal.hs +223/−0
- Text/Mustache/Type.hs +11/−11
- app/Main.hs +5/−5
- bench/Main.hs +1/−1
- mustache-spec/Spec.hs +3/−3
- specification/comments.yml +6/−0
- specification/interpolation.yml +79/−0
- specification/inverted.yml +6/−0
- specification/partials.yml +7/−0
- specification/sections.yml +81/−13
- stache.cabal +56/−56
- tests/Text/Mustache/Compile/THSpec.hs +2/−2
- tests/Text/Mustache/RenderSpec.hs +116/−1
- tests/Text/Mustache/TypeSpec.hs +1/−1
CHANGELOG.md view
@@ -1,4 +1,15 @@+## Stache 2.4.0++* Fix indentation inheritance in partials. [Issue+ 72](https://github.com/mrkkrp/stache/issues/72).++* Re-implement the render and add `Text.Mustache.Render.Internal` as a+ side-effect.++* Change the license back to BSD 3 clause.+ ## Stache 2.3.4+ * Support GHC 9.6 thanks to @ysangkok * Fix infinite loop caused by unclosed sections thanks to @ernius
− CODE_OF_CONDUCT.md
@@ -1,35 +0,0 @@-# Code of conduct--## Purpose-The primary goal of this Code of Conduct is to enable an open and welcoming environment. We pledge to making participation in our project a harassment-free experience for everyone, regardless of gender, sexual-orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).--## General recommendations-Examples of behavior that contributes to creating a positive environment include:--- Using welcoming and inclusive language-- Being respectful of differing viewpoints and experiences-- Gracefully accepting constructive criticism-- Focusing on what is best for the community-- Showing empathy towards other community members--Examples of unacceptable behavior by participants include:--- The use of sexualized language or imagery and unwelcome sexual attention or advances-- Trolling, insulting/derogatory comments, and personal or political attacks-- Public or private harassment-- Publishing others' private information, such as a physical or electronic address, without explicit permission-- Other conduct which could reasonably be considered inappropriate in a professional setting--## Maintainer responsibilities-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.--Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.--## Scope-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.--## Enforcement-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [community@stackbuilders.com](mailto:community@stackbuilders.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.--Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
− CONTRIBUITING.md
@@ -1,13 +0,0 @@--Thank you for your interest in contributing to this Stack Builders' library. To contribute, please take our [Code of Conduct](CODE_OF_CONDUCT.md) into account, along with the following recommendations:--- When submitting contributions to this repository, please make sure to discuss with the maintainer(s) the change you want to make. You can do this through an issue, or by sending an email to [community@stackbuilders.com](mailto:community@stackbuilders.com)--- Once the change has been discussed with the maintainer(s), feel free to open a Pull Request. Please include a link to the issue you're trying to solve, or a quick summary of the discussed changes.--- If adding any new features that you think should be considered in the README file, please add that information in your Pull Request.--- Once you get an approval from any of the maintainers, please merge your Pull Request. Keep in mind that some of our Stack Builders repositories use CI/CD pipelines, so you will need to pass all of the required checks before merging.--## Getting help-Contact any of our current maintainers, or send us an email at [community@stackbuilders.com](mailto:community@stackbuilders.com) for more information. Thank you for contributing!
LICENSE view
@@ -1,21 +1,28 @@-MIT License+Copyright © 2016–present Mark Karpov -Copyright (c) 2022 Stack Builders Inc.+All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy-of this software and associated documentation files (the "Software"), to deal-in the Software without restriction, including without limitation the rights-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell-copies of the Software, and to permit persons to whom the Software is-furnished to do so, subject to the following conditions:+Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in all-copies or substantial portions of the Software.+* Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE-SOFTWARE.+* 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 Mark Karpov nor the names of 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 “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 HOLDERS 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
@@ -3,7 +3,7 @@ [](https://hackage.haskell.org/package/stache) [](http://stackage.org/nightly/package/stache) [](http://stackage.org/lts/package/stache)-+[](https://github.com/mrkkrp/stache/actions/workflows/ci.yaml) This is a Haskell implementation of Mustache templates. The implementation conforms to the version 1.1.3 of the official [Mustache@@ -73,12 +73,6 @@ ## License -MIT, see [the LICENSE file](LICENSE).--## Contributing--Do you want to contribute to this project? Please take a look at our [contributing guideline](/docs/CONTRIBUTING.md) to know how you can help us build it.+Copyright © 2016–present Mark Karpov -----<img src="https://www.stackbuilders.com/media/images/Sb-supports.original.png" alt="Stack Builders" width="50%"></img>-[Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/)+Distributed under BSD 3 clause license.
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
Text/Mustache.hs view
@@ -1,6 +1,6 @@ -- | -- Module : Text.Mustache--- Copyright : © 2016–present Stack Builders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>
Text/Mustache/Compile.hs view
@@ -1,6 +1,6 @@ -- | -- Module : Text.Mustache.Compile--- Copyright : © 2016–present Stack Builders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -24,13 +24,13 @@ import Control.Exception import Control.Monad (filterM, foldM) import Control.Monad.IO.Class (MonadIO (..))-import qualified Data.Map as M+import Data.Map qualified as M import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.IO as T+import Data.Text qualified as T+import Data.Text.IO qualified as T import Data.Void import System.Directory-import qualified System.FilePath as F+import System.FilePath qualified as F import Text.Megaparsec import Text.Mustache.Parser import Text.Mustache.Type
Text/Mustache/Compile/TH.hs view
@@ -4,7 +4,7 @@ -- | -- Module : Text.Mustache.Compile.TH--- Copyright : © 2016–present Stack Builders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -25,12 +25,12 @@ import Control.Exception import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T import Language.Haskell.TH hiding (Dec) import Language.Haskell.TH.Quote (QuasiQuoter (..)) import Language.Haskell.TH.Syntax (addDependentFile, lift) import System.Directory-import qualified Text.Mustache.Compile as C+import Text.Mustache.Compile qualified as C import Text.Mustache.Type -- | Compile all templates in the specified directory and select one.
Text/Mustache/Parser.hs view
@@ -2,7 +2,7 @@ -- | -- Module : Text.Mustache.Parser--- Copyright : © 2016–present Stack Builders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -22,11 +22,11 @@ import Data.Char (isSpace) import Data.Maybe (catMaybes) import Data.Text (Text, stripEnd)-import qualified Data.Text as T+import Data.Text qualified as T import Data.Void import Text.Megaparsec import Text.Megaparsec.Char-import qualified Text.Megaparsec.Char.Lexer as L+import Text.Megaparsec.Char.Lexer qualified as L import Text.Mustache.Type ----------------------------------------------------------------------------
Text/Mustache/Render.hs view
@@ -2,7 +2,7 @@ -- | -- Module : Text.Mustache.Render--- Copyright : © 2016–present Stack Builders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -19,50 +19,22 @@ where import Control.Monad (forM_, unless, when)-import Control.Monad.Reader (MonadReader (local), ReaderT (runReaderT), asks)-import Control.Monad.State.Strict (State, execState, modify') import Data.Aeson hiding (Key)-import qualified Data.Aeson.Key as Aeson.Key-import qualified Data.Aeson.KeyMap as Aeson.KeyMap+import Data.Aeson.Key qualified as Aeson.Key+import Data.Aeson.KeyMap qualified as Aeson.KeyMap import Data.Foldable (asum) import Data.List (tails)-import Data.List.NonEmpty (NonEmpty (..))-import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M+import Data.List.NonEmpty qualified as NE import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy.Encoding as TL-import qualified Data.Vector as V-import Text.Megaparsec.Pos (Pos, mkPos, unPos)+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Builder qualified as TLB+import Data.Text.Lazy.Encoding qualified as TL+import Data.Vector qualified as V+import Text.Mustache.Render.Internal import Text.Mustache.Type ------------------------------------------------------------------------------- The rendering monad---- | Synonym for the monad we use for rendering. It allows us to share--- context and accumulate the result as 'B.Builder' data which is then--- turned into a lazy 'TL.Text'.-type Render a = ReaderT RenderContext (State S) a--data S = S ([MustacheWarning] -> [MustacheWarning]) B.Builder---- | The render monad context.-data RenderContext = RenderContext- { -- | Actual indentation level- rcIndent :: Maybe Pos,- -- | The context stack- rcContext :: NonEmpty Value,- -- | Prefix accumulated by entering sections- rcPrefix :: Key,- -- | The template to render- rcTemplate :: Template,- -- | Is this last node in this partial?- rcLastNode :: Bool- }------------------------------------------------------------------------------ -- High-level interface -- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values@@ -74,131 +46,67 @@ -- -- @since 1.1.1 renderMustacheW :: Template -> Value -> ([MustacheWarning], TL.Text)-renderMustacheW t =- runRender (renderPartial (templateActual t) Nothing renderNode) t+renderMustacheW template value =+ runR (renderTemplate (templateActual template)) value (templateCache template) +-- | Main template rendering function.+renderTemplate :: PName -> R ()+renderTemplate name = do+ mns <- lookupTemplate name+ case mns of+ Just ns -> mapM_ renderNode ns+ Nothing -> return ()++----------------------------------------------------------------------------+-- Node rendering+ -- | Render a single 'Node'.-renderNode :: Node -> Render ()-renderNode (TextBlock txt) = outputIndented txt+renderNode :: Node -> R ()+renderNode (TextBlock text) = txt IndentEveryLine text renderNode (EscapedVar k) =- lookupKey k >>= renderValue k >>= outputRaw . escapeHtml+ lookupKey k >>= renderValue k >>= txt IndentOnlyFirstLine . escapeHtml renderNode (UnescapedVar k) =- lookupKey k >>= renderValue k >>= outputRaw+ lookupKey k >>= renderValue k >>= txt IndentOnlyFirstLine renderNode (Section k ns) = do val <- lookupKey k- enterSection k $- unless (isBlank val) $+ unless (isBlank val) $+ addPrefix k $ case val of Array xs -> forM_ (V.toList xs) $ \x ->- addToLocalContext x (renderMany renderNode ns)+ addContext x (mapM_ renderNode ns) _ ->- addToLocalContext val (renderMany renderNode ns)+ addContext val (mapM_ renderNode ns) renderNode (InvertedSection k ns) = do val <- lookupKey k when (isBlank val) $- renderMany renderNode ns-renderNode (Partial pname indent) =- renderPartial pname indent renderNode+ mapM_ renderNode ns+renderNode (Partial pname mindent) = do+ mns <- lookupTemplate pname+ let adjustIndent = case mindent of+ Nothing -> id+ Just indent -> incrementIndentBy indent+ adjustIndent . resetPrefix $ case mns of+ Nothing -> txt IndentEveryLine ""+ Just ns -> mapM_ renderNode ns ------------------------------------------------------------------------------- The rendering monad vocabulary---- | Run 'Render' monad given template to render and a 'Value' to take--- values from.-runRender :: Render a -> Template -> Value -> ([MustacheWarning], TL.Text)-runRender m t v = (ws [], B.toLazyText b)- where- S ws b = execState (runReaderT m rc) (S id mempty)- rc =- RenderContext- { rcIndent = Nothing,- rcContext = v :| [],- rcPrefix = mempty,- rcTemplate = t,- rcLastNode = True- }-{-# INLINE runRender #-}---- | Output a piece of strict 'Text'.-outputRaw :: Text -> Render ()-outputRaw = tellBuilder . B.fromText-{-# INLINE outputRaw #-}---- | Output indentation consisting of appropriate number of spaces.-outputIndent :: Render ()-outputIndent = asks rcIndent >>= outputRaw . buildIndent-{-# INLINE outputIndent #-}---- | Output piece of strict 'Text' with added indentation.-outputIndented :: Text -> Render ()-outputIndented txt = do- level <- asks rcIndent- lnode <- asks rcLastNode- let f x = outputRaw (T.replace "\n" ("\n" <> buildIndent level) x)- if lnode && T.isSuffixOf "\n" txt- then f (T.init txt) >> outputRaw "\n"- else f txt-{-# INLINE outputIndented #-}---- | Render a partial.-renderPartial ::- -- | Name of partial to render- PName ->- -- | Indentation level to use- Maybe Pos ->- -- | How to render nodes in that partial- (Node -> Render ()) ->- Render ()-renderPartial pname i f =- local u (outputIndent >> getNodes >>= renderMany f)- where- u rc =- rc- { rcIndent = addIndents i (rcIndent rc),- rcPrefix = mempty,- rcTemplate = (rcTemplate rc) {templateActual = pname},- rcLastNode = True- }-{-# INLINE renderPartial #-}---- | Get collection of 'Node's for actual template.-getNodes :: Render [Node]-getNodes = do- Template actual cache <- asks rcTemplate- return (M.findWithDefault [] actual cache)-{-# INLINE getNodes #-}---- | Render many nodes.-renderMany ::- -- | How to render a node- (Node -> Render ()) ->- -- | The collection of nodes to render- [Node] ->- Render ()-renderMany _ [] = return ()-renderMany f [n] = do- ln <- asks rcLastNode- local (\rc -> rc {rcLastNode = ln && rcLastNode rc}) (f n)-renderMany f (n : ns) = do- local (\rc -> rc {rcLastNode = False}) (f n)- renderMany f ns+-- Value lookup -- | Lookup a 'Value' by its 'Key'.-lookupKey :: Key -> Render Value-lookupKey (Key []) = NE.head <$> asks rcContext+lookupKey :: Key -> R Value+lookupKey (Key []) = NE.head <$> getContext lookupKey k = do- v <- asks rcContext- p <- asks rcPrefix- let f x = asum (simpleLookup False (x <> k) <$> v)- case asum (fmap (f . Key) . reverse . tails $ unKey p) of+ context <- getContext+ prefix <- getPrefix+ let f x = asum (simpleLookup False (x <> k) <$> context)+ case asum (fmap (f . Key) . reverse . tails $ unKey prefix) of Nothing ->- Null <$ tellWarning (MustacheVariableNotFound (p <> k))+ Null <$ addWarning (MustacheVariableNotFound (prefix <> k)) Just r -> return r --- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as--- “path”.+-- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as "path". simpleLookup :: -- | At least one part of the path matched, in this case we are -- “committed” to this lookup and cannot say “there is nothing, try@@ -217,45 +125,9 @@ Nothing -> if c then Just Null else Nothing Just v -> simpleLookup True (Key ks) v simpleLookup _ _ _ = Nothing-{-# INLINE simpleLookup #-} --- | Enter the section by adding given 'Key' prefix to current prefix.-enterSection :: Key -> Render a -> Render a-enterSection p =- local (\rc -> rc {rcPrefix = p <> rcPrefix rc})-{-# INLINE enterSection #-}---- | Add new value on the top of context. The new value has the highest--- priority when lookup takes place.-addToLocalContext :: Value -> Render a -> Render a-addToLocalContext v =- local (\rc -> rc {rcContext = NE.cons v (rcContext rc)})-{-# INLINE addToLocalContext #-}- ------------------------------------------------------------------------------- Helpers---- | Register a warning.-tellWarning :: MustacheWarning -> Render ()-tellWarning w = modify' $ \(S ws b) -> S (ws . (w :)) b---- | Register a piece of output.-tellBuilder :: B.Builder -> Render ()-tellBuilder b' = modify' $ \(S ws b) -> S ws (b <> b')---- | Add two @'Maybe' 'Pos'@ values together.-addIndents :: Maybe Pos -> Maybe Pos -> Maybe Pos-addIndents Nothing Nothing = Nothing-addIndents Nothing (Just x) = Just x-addIndents (Just x) Nothing = Just x-addIndents (Just x) (Just y) = Just (mkPos $ unPos x + unPos y - 1)-{-# INLINE addIndents #-}---- | Build indentation of specified length by repeating the space character.-buildIndent :: Maybe Pos -> Text-buildIndent Nothing = ""-buildIndent (Just p) = let n = fromIntegral (unPos p) - 1 in T.replicate n " "-{-# INLINE buildIndent #-}+-- Helper functions -- | Select invisible values. isBlank :: Value -> Bool@@ -265,35 +137,31 @@ isBlank (Array a) = V.null a isBlank (String s) = T.null s isBlank _ = False-{-# INLINE isBlank #-} -- | Render Aeson's 'Value' /without/ HTML escaping.-renderValue :: Key -> Value -> Render Text+renderValue :: Key -> Value -> R Text renderValue k v = case v of Null -> return "" String str -> return str Object _ -> do- tellWarning (MustacheDirectlyRenderedValue k)- render v+ addWarning (MustacheDirectlyRenderedValue k)+ return $ TL.toStrict $ TL.decodeUtf8 $ encode v Array _ -> do- tellWarning (MustacheDirectlyRenderedValue k)- render v- _ -> render v- where- render = return . TL.toStrict . TL.decodeUtf8 . encode-{-# INLINE renderValue #-}+ addWarning (MustacheDirectlyRenderedValue k)+ return $ TL.toStrict $ TL.decodeUtf8 $ encode v+ _ -> return $ TL.toStrict $ TL.decodeUtf8 $ encode v --- | Escape HTML represented as strict 'Text'.+-- | Escape HTML represented as strict 'Text' in a single pass. escapeHtml :: Text -> Text-escapeHtml txt =- foldr- (uncurry T.replace)- txt- [ ("\"", """),- ("'", "'"),- ("<", "<"),- (">", ">"),- ("&", "&")- ]-{-# INLINE escapeHtml #-}+escapeHtml = TL.toStrict . TLB.toLazyText . T.foldl' escapeChar mempty+ where+ escapeChar :: TLB.Builder -> Char -> TLB.Builder+ escapeChar acc c =+ acc <> case c of+ '&' -> TLB.fromText "&"+ '<' -> TLB.fromText "<"+ '>' -> TLB.fromText ">"+ '"' -> TLB.fromText """+ '\'' -> TLB.fromText "'"+ _ -> TLB.singleton c
+ Text/Mustache/Render/Internal.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Text.Mustache.Render.Internal+-- Copyright : © 2016–present Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- Internal machinery of the rendering monad.+--+-- @since 2.4.0+module Text.Mustache.Render.Internal+ ( R,+ runR,+ IndentationPolicy (..),+ txt,+ incrementIndentBy,+ addContext,+ addPrefix,+ resetPrefix,+ addWarning,+ getContext,+ getPrefix,+ lookupTemplate,+ )+where++import Control.Monad.Reader (ReaderT, asks, local, runReaderT)+import Control.Monad.State.Strict (State, execState, modify')+import Data.Aeson (Value)+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE+import Data.Map (Map)+import Data.Map qualified as M+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Builder qualified as TLB+import Text.Megaparsec.Pos (Pos, mkPos, pos1, unPos)+import Text.Mustache.Type (Key, MustacheWarning, Node, PName)++-- | The 'R' monad supports printing with some helpful functionality to deal+-- with indentation.+newtype R a = R (ReaderT RC (State SC) a)+ deriving (Functor, Applicative, Monad)++-- | The reader context of 'R'.+data RC = RC+ { -- | Indentation level, as the column index we need to start from after+ -- a newline+ rcIndent :: !Pos,+ -- | The context stack+ rcContext :: NonEmpty Value,+ -- | The prefix accumulated by entering sections+ rcPrefix :: Key,+ -- | The template cache+ rcTemplateCache :: Map PName [Node]+ }++-- | The state context of 'R'.+data SC = SC+ { -- | Index of the next column to render+ scColumn :: !Pos,+ -- | Are we at the beginning of a newline?+ scBeginningOfLine :: !Bool,+ -- | Mustache warnings+ scWarnings :: [MustacheWarning] -> [MustacheWarning],+ -- | The output we have rendered so far (using Builder for efficiency)+ scBuilder :: TLB.Builder+ }++-- | The indentation policy to use when rendering text.+data IndentationPolicy+ = -- | Indent every line+ IndentEveryLine+ | -- | Only indent the very first line, provided we are at the beginning+ -- of the line+ IndentOnlyFirstLine++-- | Run the 'R' monad.+runR ::+ -- | Monad to run+ R () ->+ -- | The Context to use+ Value ->+ -- | The template cache+ Map PName [Node] ->+ ([MustacheWarning], TL.Text)+runR (R m) v cache = (scWarnings s [], TLB.toLazyText (scBuilder s))+ where+ rc =+ RC+ { rcIndent = pos1,+ rcContext = v :| [],+ rcPrefix = mempty,+ rcTemplateCache = cache+ }+ sc =+ SC+ { scColumn = pos1,+ scBeginningOfLine = True,+ scWarnings = id,+ scBuilder = mempty+ }+ s = execState (runReaderT m rc) sc++-- | Output text, handling indentation appropriately. When we're at the+-- beginning of a line and the indentation level is non-zero, we first+-- output the appropriate amount of spaces.+txt ::+ -- | Indentation policy to use+ IndentationPolicy ->+ -- | The text to output+ Text ->+ R ()+txt _ "" = R $ do+ indent <- asks rcIndent+ modify' $ \sc ->+ if scBeginningOfLine sc && indent > pos1+ then+ let indentText = T.replicate (unPos indent - 1) " "+ in sc+ { scColumn = mkPos (unPos indent),+ scBeginningOfLine = False,+ scBuilder = scBuilder sc <> TLB.fromText indentText+ }+ else sc+txt policy t = R $ do+ indent <- asks rcIndent+ modify' $ \sc ->+ let processLine isFirstLine line sc' =+ let shouldIndent = case policy of+ IndentEveryLine ->+ scBeginningOfLine sc'+ && not (T.null line)+ && indent > pos1+ IndentOnlyFirstLine ->+ isFirstLine+ && scBeginningOfLine sc'+ && not (T.null line)+ && indent > pos1+ indentText =+ if shouldIndent+ then T.replicate (unPos indent - 1) " "+ else T.empty+ indentedLine = indentText <> line+ newColumn =+ if T.null indentedLine+ then scColumn sc'+ else mkPos (unPos (scColumn sc') + T.length indentedLine)+ in sc'+ { scColumn = newColumn,+ scBeginningOfLine = T.null indentedLine && scBeginningOfLine sc',+ scBuilder = scBuilder sc' <> TLB.fromText indentedLine+ }+ go _ [] sc' = sc'+ go isFirst [line] sc' = processLine isFirst line sc'+ go isFirst (line : rest) sc' =+ let sc'' = processLine isFirst line sc'+ sc''' =+ sc''+ { scColumn = pos1,+ scBeginningOfLine = True,+ scBuilder = scBuilder sc'' <> TLB.singleton '\n'+ }+ in go False rest sc'''+ lines' = T.lines t+ endsWithNewline = not (T.null t) && T.last t == '\n'+ processedState = go True lines' sc+ in if endsWithNewline+ then+ processedState+ { scColumn = pos1,+ scBeginningOfLine = True,+ scBuilder = scBuilder processedState <> TLB.singleton '\n'+ }+ else processedState++-- | Increment the indentation level by the given amount.+incrementIndentBy :: Pos -> R a -> R a+incrementIndentBy n (R m) =+ R $+ local (\rc -> rc {rcIndent = mkPos (unPos (rcIndent rc) + unPos n - 1)}) m++-- | Add a value to the context stack.+addContext :: Value -> R a -> R a+addContext v (R m) =+ R $+ local (\rc -> rc {rcContext = NE.cons v (rcContext rc)}) m++-- | Add to the key prefix.+addPrefix :: Key -> R a -> R a+addPrefix key (R m) =+ R $+ local (\rc -> rc {rcPrefix = rcPrefix rc <> key}) m++-- | Reset prefix.+resetPrefix :: R a -> R a+resetPrefix (R m) =+ R $+ local (\rc -> rc {rcPrefix = mempty}) m++-- | Add a warning to the warnings list.+addWarning :: MustacheWarning -> R ()+addWarning w = R $+ modify' $+ \sc -> sc {scWarnings = scWarnings sc . (w :)}++-- | Get the current context stack.+getContext :: R (NonEmpty Value)+getContext = R $ asks rcContext++-- | Get the current key prefix.+getPrefix :: R Key+getPrefix = R $ asks rcPrefix++-- | Lookup the template by its name.+lookupTemplate :: PName -> R (Maybe [Node])+lookupTemplate name = R $ asks $ \rc ->+ M.lookup name (rcTemplateCache rc)
Text/Mustache/Type.hs view
@@ -6,7 +6,7 @@ -- | -- Module : Text.Mustache.Type--- Copyright : © 2016–present Stack Buliders+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -32,14 +32,14 @@ import Control.Exception (Exception (..)) import Data.Data (Data) import Data.Map (Map)-import qualified Data.Map as M+import Data.Map qualified as M import Data.String (IsString (..)) import Data.Text (Text)-import qualified Data.Text as T-import Data.Typeable (Typeable, cast)+import Data.Text qualified as T+import Data.Typeable (cast) import Data.Void import GHC.Generics-import qualified Language.Haskell.TH.Syntax as TH+import Language.Haskell.TH.Syntax qualified as TH import Text.Megaparsec -- | Mustache template as the name of the “top-level” template and a@@ -57,7 +57,7 @@ -- “focus” can be switched easily by modifying 'templateActual'. templateCache :: Map PName [Node] }- deriving (Eq, Ord, Show, Data, Typeable, Generic)+ deriving (Eq, Ord, Show, Data, Generic) instance Semigroup Template where (Template pname x) <> (Template _ y) = Template pname (M.union x y)@@ -81,7 +81,7 @@ InvertedSection Key [Node] | -- | Partial with indentation level ('Nothing' means it was inlined) Partial PName (Maybe Pos)- deriving (Eq, Ord, Show, Data, Typeable, Generic)+ deriving (Eq, Ord, Show, Data, Generic) -- | @since 2.1.0 instance TH.Lift Node where@@ -96,7 +96,7 @@ -- * @[text]@—single key is a normal identifier; -- * @[text1, text2]@—multiple keys represent dotted names. newtype Key = Key {unKey :: [Text]}- deriving (Eq, Ord, Show, Semigroup, Monoid, Data, Typeable, Generic)+ deriving (Eq, Ord, Show, Semigroup, Monoid, Data, Generic) instance NFData Key @@ -116,7 +116,7 @@ -- | Identifier for partials. Note that with the @OverloadedStrings@ -- extension you can use just string literals to create values of this type. newtype PName = PName {unPName :: Text}- deriving (Eq, Ord, Show, Data, Typeable, Generic)+ deriving (Eq, Ord, Show, Data, Generic) instance IsString PName where fromString = PName . T.pack@@ -137,7 +137,7 @@ -- -- /The 'Text' field was added in version 1.0.0./ MustacheParserException (ParseErrorBundle Text Void)- deriving (Eq, Show, Typeable, Generic)+ deriving (Eq, Show, Generic) instance Exception MustacheException where displayException (MustacheParserException b) = errorBundlePretty b@@ -152,7 +152,7 @@ | -- | A complex value such as an 'Object' or 'Array' was directly -- rendered into the template. MustacheDirectlyRenderedValue Key- deriving (Eq, Show, Typeable, Generic)+ deriving (Eq, Show, Generic) -- | Pretty-print a 'MustacheWarning'. --
app/Main.hs view
@@ -4,15 +4,15 @@ module Main (main) where import Data.Aeson (Value (..))-import qualified Data.Aeson as Aeson-import qualified Data.Aeson.KeyMap as Aeson.KeyMap+import Data.Aeson qualified as Aeson+import Data.Aeson.KeyMap qualified as Aeson.KeyMap import Data.List (foldl') import Data.List.NonEmpty (NonEmpty (..))-import qualified Data.List.NonEmpty as NE+import Data.List.NonEmpty qualified as NE import Data.Semigroup (sconcat)-import qualified Data.Text.Lazy.IO as T+import Data.Text.Lazy.IO qualified as T import Data.Version (showVersion)-import qualified Data.Yaml as Yaml+import Data.Yaml qualified as Yaml import Development.GitRev import Options.Applicative import Paths_stache (version)
bench/Main.hs view
@@ -7,7 +7,7 @@ import Criterion.Main import Data.Aeson import Data.Text (Text)-import qualified Data.Text.IO as T+import Data.Text.IO qualified as T import Text.Megaparsec import Text.Mustache.Compile import Text.Mustache.Parser
mustache-spec/Spec.hs view
@@ -9,10 +9,10 @@ import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Data.Map (Map, (!))-import qualified Data.Map as M+import Data.Map qualified as M import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL import Data.Yaml import Test.Hspec import Text.Megaparsec
specification/comments.yml view
@@ -101,3 +101,9 @@ data: { } template: '12345 {{! Comment Block! }} 67890' expected: '12345 67890'++ - name: Variable Name Collision+ desc: Comments must never render, even if variable with same name exists.+ data: { '! comment': 1, '! comment ': 2, '!comment': 3, 'comment': 4}+ template: 'comments never show: >{{! comment }}<'+ expected: 'comments never show: ><'
specification/interpolation.yml view
@@ -41,6 +41,12 @@ expected: | Hello, world! + - name: No Re-interpolation+ desc: Interpolated tag output should not be re-interpolated.+ data: { template: '{{planet}}', planet: 'Earth' }+ template: '{{template}}: {{planet}}'+ expected: '{{planet}}: Earth'+ - name: HTML Escaping desc: Basic interpolation should be HTML escaped. data: { forbidden: '& " < >' }@@ -101,6 +107,24 @@ template: '"{{&power}} jiggawatts!"' expected: '"1.21 jiggawatts!"' + - name: Basic Null Interpolation+ desc: Nulls should interpolate as the empty string.+ data: { cannot: null }+ template: "I ({{cannot}}) be seen!"+ expected: "I () be seen!"++ - name: Triple Mustache Null Interpolation+ desc: Nulls should interpolate as the empty string.+ data: { cannot: null }+ template: "I ({{{cannot}}}) be seen!"+ expected: "I () be seen!"++ - name: Ampersand Null Interpolation+ desc: Nulls should interpolate as the empty string.+ data: { cannot: null }+ template: "I ({{&cannot}}) be seen!"+ expected: "I () be seen!"+ # Context Misses - name: Basic Context Miss Interpolation@@ -178,6 +202,61 @@ b: { c: 'ERROR' } template: '{{#a}}{{b.c}}{{/a}}' expected: ''++ - name: Dotted Names are never single keys+ desc: Dotted names shall not be parsed as single, atomic keys+ data:+ a.b: c+ template: '{{a.b}}'+ expected: ''++ - name: Dotted Names - No Masking+ desc: Dotted Names in a given context are unvavailable due to dot splitting+ data:+ a.b: c+ a: { b: d }+ template: '{{a.b}}'+ expected: 'd'++ # Implicit Iterators++ - name: Implicit Iterators - Basic Interpolation+ desc: Unadorned tags should interpolate content into the template.+ data: "world"+ template: |+ Hello, {{.}}!+ expected: |+ Hello, world!++ - name: Implicit Iterators - HTML Escaping+ desc: Basic interpolation should be HTML escaped.+ data: '& " < >'+ template: |+ These characters should be HTML escaped: {{.}}+ expected: |+ These characters should be HTML escaped: & " < >++ - name: Implicit Iterators - Triple Mustache+ desc: Triple mustaches should interpolate without HTML escaping.+ data: '& " < >'+ template: |+ These characters should not be HTML escaped: {{{.}}}+ expected: |+ These characters should not be HTML escaped: & " < >++ - name: Implicit Iterators - Ampersand+ desc: Ampersand should interpolate without HTML escaping.+ data: '& " < >'+ template: |+ These characters should not be HTML escaped: {{&.}}+ expected: |+ These characters should not be HTML escaped: & " < >++ - name: Implicit Iterators - Basic Integer Interpolation+ desc: Integers should interpolate seamlessly.+ data: 85+ template: '"{{.}} miles an hour!"'+ expected: '"85 miles an hour!"' # Whitespace Sensitivity
specification/inverted.yml view
@@ -46,6 +46,12 @@ template: '"{{^boolean}}This should not be rendered.{{/boolean}}"' expected: '""' + - name: Null is falsey+ desc: Null is falsey.+ data: { "null": null }+ template: '"{{^null}}This should be rendered.{{/null}}"'+ expected: '"This should be rendered."'+ - name: Context desc: Objects and hashes should behave like truthy values. data: { context: { name: 'Joe' } }
specification/partials.yml view
@@ -42,6 +42,13 @@ partials: { node: '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>' } expected: 'X<Y<>>' + - name: Nested+ desc: The greater-than operator should work from within partials.+ data: { a: "hello", b: "world" }+ template: '{{>outer}}'+ partials: { outer: '*{{a}} {{>inner}}*', inner: '{{b}}!' }+ expected: '*hello world!*'+ # Whitespace Sensitivity - name: Surrounding Whitespace
specification/sections.yml view
@@ -1,29 +1,32 @@ overview: | Section tags and End Section tags are used in combination to wrap a section- of the template for iteration+ of the template for iteration. These tags' content MUST be a non-whitespace character sequence NOT containing the current closing delimiter; each Section tag MUST be followed by an End Section tag with the same content within the same section. - This tag's content names the data to replace the tag. Name resolution is as+ This tag's content names the data to replace the tag. Name resolution is as follows:- 1) Split the name on periods; the first part is the name to resolve, any+ 1) If the name is a single period (.), the data is the item currently+ sitting atop the context stack. Skip the rest of these steps.+ 2) Split the name on periods; the first part is the name to resolve, any remaining parts should be retained.- 2) Walk the context stack from top to bottom, finding the first context+ 3) Walk the context stack from top to bottom, finding the first context that is a) a hash containing the name as a key OR b) an object responding to a method with the given name.- 3) If the context is a hash, the data is the value associated with the+ 4) If the context is a hash, the data is the value associated with the name.- 4) If the context is an object and the method with the given name has an+ 5) If the context is an object and the method with the given name has an arity of 1, the method SHOULD be called with a String containing the unprocessed contents of the sections; the data is the value returned.- 5) Otherwise, the data is the value returned by calling the method with+ 6) Otherwise, the data is the value returned by calling the method with the given name.- 6) If any name parts were retained in step 1, each should be resolved+ 7) If any name parts were retained in step 1, each should be resolved against a context stack containing only the result from the former resolution. If any part fails resolution, the result should be considered falsey, and should interpolate as the empty string.+ If the data is not of a list type, it is coerced into a list as follows: if the data is truthy (e.g. `!!data == true`), use a single-element list containing the data, otherwise use an empty list.@@ -47,20 +50,54 @@ template: '"{{#boolean}}This should not be rendered.{{/boolean}}"' expected: '""' + - name: Null is falsey+ desc: Null is falsey.+ data: { "null": null }+ template: '"{{#null}}This should not be rendered.{{/null}}"'+ expected: '""'+ - name: Context desc: Objects and hashes should be pushed onto the context stack. data: { context: { name: 'Joe' } } template: '"{{#context}}Hi {{name}}.{{/context}}"' expected: '"Hi Joe."' + - name: Parent contexts+ desc: Names missing in the current context are looked up in the stack.+ data: { a: "foo", b: "wrong", sec: { b: "bar" }, c : { d : "baz" } }+ template: '"{{#sec}}{{a}}, {{b}}, {{c.d}}{{/sec}}"'+ expected: '"foo, bar, baz"'++ - name: Variable test+ desc: |+ Non-false sections have their value at the top of context,+ accessible as {{.}} or through the parent context. This gives+ a simple way to display content conditionally if a variable exists.+ data: { foo: "bar" }+ template: '"{{#foo}}{{.}} is {{foo}}{{/foo}}"'+ expected: '"bar is bar"'++ - name: List Contexts+ desc: All elements on the context stack should be accessible within lists.+ data:+ tops:+ - tname:+ upper: "A"+ lower: "a"+ middles:+ - mname: "1"+ bottoms:+ - bname: "x"+ - bname: "y"+ template: '{{#tops}}{{#middles}}{{tname.lower}}{{mname}}.{{#bottoms}}{{tname.upper}}{{mname}}{{bname}}.{{/bottoms}}{{/middles}}{{/tops}}'+ expected: 'a1.A1x.A1y.'+ - name: Deeply Nested Contexts desc: All elements on the context stack should be accessible. data: a: { one: 1 } b: { two: 2 }- c: { three: 3 }- d: { four: 4 }- e: { five: 5 }+ c: { three: 3, d : { four : 4, five : 5 } } template: | {{#a}} {{one}}@@ -70,9 +107,11 @@ {{one}}{{two}}{{three}}{{two}}{{one}} {{#d}} {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}- {{#e}}+ {{#five}} {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}- {{/e}}+ {{one}}{{two}}{{three}}{{four}}{{.}}6{{.}}{{four}}{{three}}{{two}}{{one}}+ {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}+ {{/five}} {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} {{/d}} {{one}}{{two}}{{three}}{{two}}{{one}}@@ -87,6 +126,8 @@ 12321 1234321 123454321+ 12345654321+ 123454321 1234321 12321 121@@ -167,6 +208,33 @@ list: [ [1, 2, 3], ['a', 'b', 'c'] ] template: '"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"' expected: '"(123)(abc)"'++ - name: Implicit Iterator - HTML Escaping+ desc: Implicit iterators with basic interpolation should be HTML escaped.+ data:+ list: [ '&', '"', '<', '>' ]+ template: '"{{#list}}({{.}}){{/list}}"'+ expected: '"(&)(")(<)(>)"'++ - name: Implicit Iterator - Triple mustache+ desc: Implicit iterators in triple mustache should interpolate without HTML escaping.+ data:+ list: [ '&', '"', '<', '>' ]+ template: '"{{#list}}({{{.}}}){{/list}}"'+ expected: '"(&)(")(<)(>)"'++ - name: Implicit Iterator - Ampersand+ desc: Implicit iterators in an Ampersand tag should interpolate without HTML escaping.+ data:+ list: [ '&', '"', '<', '>' ]+ template: '"{{#list}}({{&.}}){{/list}}"'+ expected: '"(&)(")(<)(>)"'++ - name: Implicit Iterator - Root-level+ desc: Implicit iterators should work on root-level lists.+ data: [ { value: 'a' }, { value: 'b' } ]+ template: '"{{#.}}({{value}}){{/.}}"'+ expected: '"(a)(b)"' # Dotted Names
stache.cabal view
@@ -1,13 +1,13 @@ cabal-version: 2.4 name: stache-version: 2.3.4-license: MIT+version: 2.4.0+license: BSD-3-Clause license-file: LICENSE maintainer: Mark Karpov <markkarpov92@gmail.com> author: Mark Karpov <markkarpov92@gmail.com>-tested-with: ghc ==9.0.2 ghc ==9.2.8 ghc ==9.4.5 ghc ==9.6.2-homepage: https://github.com/stackbuilders/stache-bug-reports: https://github.com/stackbuilders/stache/issues+tested-with: ghc ==9.10.3 ghc ==9.12.4 ghc ==9.14.1+homepage: https://github.com/mrkkrp/stache+bug-reports: https://github.com/mrkkrp/stache/issues synopsis: Mustache templates for Haskell description: Mustache templates for Haskell. category: Text@@ -19,13 +19,11 @@ extra-doc-files: CHANGELOG.md- CODE_OF_CONDUCT.md- CONTRIBUITING.md README.md source-repository head type: git- location: https://github.com/stackbuilders/stache.git+ location: https://github.com/mrkkrp/stache.git flag dev description: Turn on development settings.@@ -39,54 +37,51 @@ Text.Mustache.Compile.TH Text.Mustache.Parser Text.Mustache.Render+ Text.Mustache.Render.Internal Text.Mustache.Type - default-language: Haskell2010+ default-language: GHC2021 build-depends: aeson >=2 && <3,- base >=4.15 && <5.0,- bytestring >=0.10 && <0.12,- containers >=0.5 && <0.7,- deepseq >=1.4 && <1.5,+ base >=4.15 && <5,+ containers >=0.5 && <0.9,+ deepseq >=1.4 && <1.6, directory >=1.2 && <1.4,- filepath >=1.2 && <1.5,- megaparsec >=7.0 && <10.0,- mtl >=2.1 && <3.0,- template-haskell >=2.11 && <2.21,- text >=1.2 && <2.1,+ filepath >=1.2 && <1.6,+ megaparsec >=7 && <10,+ mtl >=2.1 && <3,+ template-haskell >=2.11 && <2.25,+ text >=1.2 && <2.2, vector >=0.11 && <0.14 if flag(dev)- ghc-options: -O0 -Wall -Werror -fsimpl-tick-factor=150+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else- ghc-options: -O2 -Wall -fsimpl-tick-factor=150-- if flag(dev)- ghc-options:- -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns- -Wnoncanonical-monad-instances+ ghc-options: -O2 -Wall executable stache main-is: Main.hs hs-source-dirs: app other-modules: Paths_stache autogen-modules: Paths_stache- default-language: Haskell2010+ default-language: GHC2021 build-depends: aeson >=2 && <3,- base >=4.15 && <5.0,+ base >=4.15 && <5, gitrev >=1.3 && <1.4,- optparse-applicative >=0.14 && <0.19,+ optparse-applicative >=0.14 && <0.20, stache,- text >=0.2 && <2.1,+ text >=0.2 && <2.2, yaml >=0.8 && <0.12,- filepath >=1.2 && <1.5+ filepath >=1.2 && <1.6 if flag(dev) ghc-options:- -Wall -Werror -Wcompat -Wincomplete-record-updates- -Wincomplete-uni-patterns -Wnoncanonical-monad-instances+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages -Wno-unused-imports else ghc-options: -O2 -Wall@@ -94,7 +89,7 @@ test-suite tests type: exitcode-stdio-1.0 main-is: Spec.hs- build-tool-depends: hspec-discover:hspec-discover >=2.0 && <3.0+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3 hs-source-dirs: tests other-modules: Text.Mustache.Compile.THSpec@@ -102,20 +97,21 @@ Text.Mustache.RenderSpec Text.Mustache.TypeSpec - default-language: Haskell2010+ default-language: GHC2021 build-depends: aeson >=2 && <3,- base >=4.15 && <5.0,- containers >=0.5 && <0.7,- hspec >=2.0 && <3.0,- hspec-megaparsec >=2.0 && <3.0,- megaparsec >=7.0 && <10.0,+ base >=4.15 && <5,+ containers >=0.5 && <0.9,+ hspec >=2 && <3,+ hspec-megaparsec >=2 && <3,+ megaparsec >=7 && <10, stache,- template-haskell >=2.11 && <2.21,- text >=1.2 && <2.1+ text >=1.2 && <2.2 if flag(dev)- ghc-options: -O0 -Wall -Werror+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall@@ -124,21 +120,23 @@ type: exitcode-stdio-1.0 main-is: Spec.hs hs-source-dirs: mustache-spec- default-language: Haskell2010+ default-language: GHC2021 build-depends: aeson >=2 && <3,- base >=4.15 && <5.0,- bytestring >=0.10 && <0.12,- containers >=0.5 && <0.7,+ base >=4.15 && <5,+ bytestring >=0.10 && <0.13,+ containers >=0.5 && <0.9, file-embed,- hspec >=2.0 && <3.0,- megaparsec >=7.0 && <10.0,+ hspec >=2 && <3,+ megaparsec >=7 && <10, stache,- text >=1.2 && <2.1,+ text >=1.2 && <2.2, yaml >=0.8 && <0.12 if flag(dev)- ghc-options: -Wall -Werror+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall@@ -147,18 +145,20 @@ type: exitcode-stdio-1.0 main-is: Main.hs hs-source-dirs: bench- default-language: Haskell2010+ default-language: GHC2021 build-depends: aeson >=2 && <3,- base >=4.15 && <5.0,+ base >=4.15 && <5, criterion >=0.6.2.1 && <1.7,- deepseq >=1.4 && <1.5,- megaparsec >=7.0 && <10.0,+ deepseq >=1.4 && <1.6,+ megaparsec >=7 && <10, stache,- text >=1.2 && <2.1+ text >=1.2 && <2.2 if flag(dev)- ghc-options: -O2 -Wall -Werror+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages else ghc-options: -O2 -Wall
tests/Text/Mustache/Compile/THSpec.hs view
@@ -8,9 +8,9 @@ ) where -import qualified Data.Map as M+import Data.Map qualified as M import Test.Hspec-import qualified Text.Mustache.Compile.TH as TH+import Text.Mustache.Compile.TH qualified as TH import Text.Mustache.Type main :: IO ()
tests/Text/Mustache/RenderSpec.hs view
@@ -7,7 +7,7 @@ where import Data.Aeson (KeyValue (..), Value (..), object)-import qualified Data.Map as M+import Data.Map qualified as M import Data.Text (Text) import Test.Hspec import Text.Megaparsec@@ -152,6 +152,121 @@ ] in renderMustache template Null `shouldBe` " one\n two\n three*"+ context "when rendering a partial with list sections" $ do+ it "maintains indentation for all iterations in a partial with sections" $+ let template =+ Template "test" $+ M.fromList+ [ ("test", [TextBlock "Subnets:\n", Partial "myPartial" (Just $ mkPos 3)]),+ ( "myPartial",+ [ Section+ (key "subnets")+ [ TextBlock "- ",+ EscapedVar (Key []),+ TextBlock "\n\"Test string\"\n"+ ]+ ]+ )+ ]+ value =+ object+ [ "subnets"+ .= [ "subnet-0a0a0a0a" :: Text,+ "subnet-0b0b0b0b",+ "subnet-0c0c0c0c"+ ]+ ]+ in renderMustache template value+ `shouldBe` "Subnets:\n - subnet-0a0a0a0a\n \"Test string\"\n - subnet-0b0b0b0b\n \"Test string\"\n - subnet-0c0c0c0c\n \"Test string\"\n"+ context "when rendering nested partials with list sections" $ do+ it "maintains indentation through multiple levels of nested partials with sections" $+ let template =+ Template "test" $+ M.fromList+ [ ("test", [TextBlock "Configuration:\n", Partial "level1" (Just $ mkPos 3)]),+ ( "level1",+ [ TextBlock "Services:\n",+ Partial "level2" (Just $ mkPos 3)+ ]+ ),+ ( "level2",+ [ Section+ (key "services")+ [ TextBlock "- name: ",+ EscapedVar (key "name"),+ TextBlock "\n port: ",+ EscapedVar (key "port"),+ TextBlock "\n"+ ]+ ]+ )+ ]+ value =+ object+ [ "services"+ .= [ object ["name" .= ("web" :: Text), "port" .= (8080 :: Int)],+ object ["name" .= ("api" :: Text), "port" .= (3000 :: Int)],+ object ["name" .= ("db" :: Text), "port" .= (5432 :: Int)]+ ]+ ]+ in renderMustache template value+ `shouldBe` "Configuration:\n Services:\n - name: web\n port: 8080\n - name: api\n port: 3000\n - name: db\n port: 5432\n"+ it "handles mixed content with nested partials and multiple sections" $+ let template =+ Template "test" $+ M.fromList+ [ ( "test",+ [ TextBlock "Project:\n",+ Section+ (key "project")+ [ TextBlock " Name: ",+ EscapedVar (key "name"),+ TextBlock "\n",+ Partial "components" (Just $ mkPos 3)+ ]+ ]+ ),+ ( "components",+ [ TextBlock "Components:\n",+ Section+ (key "components")+ [ Partial "component-detail" (Just $ mkPos 3)+ ]+ ]+ ),+ ( "component-detail",+ [ TextBlock "- ",+ EscapedVar (key "type"),+ TextBlock ":\n",+ Section+ (key "items")+ [ TextBlock " * ",+ EscapedVar (Key []),+ TextBlock "\n"+ ]+ ]+ )+ ]+ value =+ object+ [ "project"+ .= [ object+ [ "name" .= ("MyApp" :: Text),+ "components"+ .= [ object+ [ "type" .= ("Frontend" :: Text),+ "items" .= ["React" :: Text, "TypeScript", "CSS"]+ ],+ object+ [ "type" .= ("Backend" :: Text),+ "items" .= ["Node.js" :: Text, "Express", "MongoDB"]+ ]+ ]+ ]+ ]+ ]+ in renderMustache template value+ `shouldBe` "Project:\n Name: MyApp\n Components:\n - Frontend:\n * React\n * TypeScript\n * CSS\n - Backend:\n * Node.js\n * Express\n * MongoDB\n" context "when rendering a nested partial" $ it "renders outer partial correctly" $ let template =
tests/Text/Mustache/TypeSpec.hs view
@@ -6,7 +6,7 @@ ) where -import qualified Data.Map as M+import Data.Map qualified as M import Test.Hspec import Text.Mustache.Type