microstache-1.0.2.3/0000755000000000000000000000000007346545000012345 5ustar0000000000000000microstache-1.0.2.3/CHANGELOG.md0000644000000000000000000000064207346545000014160 0ustar0000000000000000## microstache 1.0.2.3 - Support `parsec-3.1.16.*` ## microstache 1.0.2.1 - Support `transformers-0.6` ## microstache 1.0.2 - Support `aeson-2.0.0.0` ## microstache 1.0.1.2 - Drop `bytestring` dependency (there weren't direct one) ## microstache 1.0.1.1 - Fix build against `transformers-0.3` ## microstache 1.0.1 - Add `renderMustacheW` ## microstache 1 Initial release, corresponding to `stache-0.2.2`. microstache-1.0.2.3/LICENSE0000644000000000000000000000270107346545000013352 0ustar0000000000000000Copyright © 2016–2017 Stack Builders, 2017 Oleg Grenrus All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. microstache-1.0.2.3/README.md0000644000000000000000000000467307346545000013636 0ustar0000000000000000# microstache Based on [`stache`](http://hackage.haskell.org/package/stache) library, which uses `megaparsec`. This library uses `parsec`, thus the name: `microstache`. This is a Haskell implementation of Mustache templates. The implementation conforms to the version 1.1.3 of official [Mustache specification] (https://github.com/mustache/spec). It is extremely simple and straightforward to use with minimal but complete API — three functions to compile templates (from directory, from file, and from lazy text) and one to render them. For rendering you only need to create Aeson's `Value` where you put the data to interpolate. Since the library re-uses Aeson's instances and most data types in Haskell ecosystem are instances of classes like `Data.Aeson.ToJSON`, the whole process is very simple for end user. One feature that is not currently supported is lambdas. The feature is marked as optional in the spec and can be emulated via processing of parsed template representation. The decision to drop lambdas is intentional, for the sake of simplicity and better integration with Aeson. ## Differences from `stache` - Instead of `megaparsec`, `parsec` is used. Error message quality is most likely degraded. - There are no TemplateHaskell used; yet there are no helpers provided. - Support for GHC-7.4.2 – GHC-8.2.1 ## Quick start Here is an example of basic usage: ```haskell {-# LANGUAGE OverloadedStrings #-} module Main (main) where import Data.Aeson import Data.Text import Text.Microstache import qualified Data.Text.Lazy.IO as TIO main :: IO () main = do let res = compileMustacheText "foo" "Hi, {{name}}! You have:\n{{#things}}\n * {{.}}\n{{/things}}\n" case res of Left err -> putStrLn (show err) Right template -> TIO.putStr $ renderMustache template $ object [ "name" .= ("John" :: Text) , "things" .= ["pen" :: Text, "candle", "egg"] ] ``` If I run the program, it prints the following: ``` Hi, John! You have: * pen * candle * egg ``` For more information about Mustache templates the following links may be helpful: * The official Mustache site: https://mustache.github.io/ * The manual: https://mustache.github.io/mustache.5.html * The specification: https://github.com/mustache/spec * Stack Builders Stache tutorial: https://www.stackbuilders.com/tutorials/haskell/mustache-templates/ ## License Copyright © 2016–2017 Stack Builders, 2017 Oleg Grenrus Distributed under BSD 3 clause license. microstache-1.0.2.3/Setup.hs0000644000000000000000000000012507346545000013777 0ustar0000000000000000module Main (main) where import Distribution.Simple main :: IO () main = defaultMain microstache-1.0.2.3/microstache.cabal0000644000000000000000000000621107346545000015632 0ustar0000000000000000name: microstache version: 1.0.2.3 cabal-version: >=1.10 license: BSD3 license-file: LICENSE author: Mark Karpov , Oleg Grenrus maintainer: Oleg Grenrus homepage: https://github.com/haskellari/microstache bug-reports: https://github.com/haskellari/microstache/issues category: Text synopsis: Mustache templates for Haskell build-type: Simple description: Mustache templates for Haskell. . Based on @stache@ library, which uses @megaparsec@. This library uses @parsec@, thus the name: @microstache@. extra-source-files: CHANGELOG.md README.md specification/comments.json specification/delimiters.json specification/interpolation.json specification/inverted.json specification/partials.json specification/sections.json tested-with: GHC ==7.4.2 || ==7.6.3 || ==7.8.4 || ==7.10.3 || ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.1 source-repository head type: git location: https://github.com/haskellari/microstache.git library build-depends: aeson >=0.11 && <1.6 || >=2.0.0.0 && <2.2 , base >=4.5 && <4.18 , containers >=0.4.2.1 && <0.7 , deepseq >=1.3.0.0 && <1.5 , directory >=1.1.0.2 && <1.4 , filepath >=1.3.0.0 && <1.5 , parsec >=3.1.11 && <3.2 , text >=1.2.3.0 && <1.3 || >=2.0 && <2.1 , transformers >=0.3.0.0 && <0.7 , unordered-containers >=0.2.5 && <0.3 , vector >=0.11 && <0.14 if impl(ghc <=7.6) build-depends: ghc-prim if !impl(ghc >=8.0) build-depends: semigroups >=0.18 && <0.21 exposed-modules: Text.Microstache Text.Microstache.Compile Text.Microstache.Parser Text.Microstache.Render Text.Microstache.Type hs-source-dirs: src ghc-options: -Wall default-language: Haskell2010 test-suite spec main-is: Spec.hs hs-source-dirs: tests tasty-as-hspec type: exitcode-stdio-1.0 build-depends: aeson , base , containers , microstache , parsec , text -- tasty-as-hspec build-depends: base-orphans >=0.8.7 && <0.9 , tasty >=1.4.0.1 && <1.5 , tasty-hunit >=0.10.0.3 && <0.11 if !impl(ghc >=8.0) build-depends: semigroups other-modules: Test.Hspec Text.Microstache.ParserSpec Text.Microstache.RenderSpec Text.Microstache.TypeSpec default-language: Haskell2010 test-suite mustache-spec main-is: Spec.hs hs-source-dirs: mustache-spec tasty-as-hspec type: exitcode-stdio-1.0 build-depends: aeson , base , bytestring , containers , microstache , parsec , text -- tasty-as-hspec build-depends: base-orphans >=0.8.7 && <0.9 , tasty >=1.4.0.1 && <1.5 , tasty-hunit >=0.10.0.3 && <0.11 other-modules: Test.Hspec default-language: Haskell2010 microstache-1.0.2.3/mustache-spec/0000755000000000000000000000000007346545000015106 5ustar0000000000000000microstache-1.0.2.3/mustache-spec/Spec.hs0000644000000000000000000000613207346545000016336 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Main (main) where import Control.Monad import Data.Aeson import Data.ByteString (ByteString) import Data.Map (Map, (!)) import Data.Text (Text) import Test.Hspec import Text.Parsec import Text.Microstache import Text.Microstache.Parser import qualified Data.ByteString.Lazy as BSL import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Text.Lazy as TL -- | Representation of information contained in a Mustache spec file. data SpecFile = SpecFile { specOverview :: Text -- ^ Textual overview of this spec file , specTests :: [Test] -- ^ The actual collection of tests } instance FromJSON SpecFile where parseJSON = withObject "Mustache spec file" $ \o -> do specOverview <- o .: "overview" specTests <- o .: "tests" return SpecFile {..} -- | Representation of a single test. data Test = Test { testName :: String , testDesc :: String , testData :: Value , testTemplate :: TL.Text , testExpected :: TL.Text , testPartials :: Map Text TL.Text } instance FromJSON Test where parseJSON = withObject "Test" $ \o -> do testName <- o .: "name" testDesc <- o .: "desc" testData <- o .: "data" testTemplate <- o .: "template" testExpected <- o .: "expected" testPartials <- o .:? "partials" .!= M.empty return Test {..} main :: IO () main = spec >>= hspec spec :: IO Spec spec = do s1 <- specData "Comments" "specification/comments.json" s2 <- specData "Delimiters" "specification/delimiters.json" s3 <- specData "Interpolation" "specification/interpolation.json" s4 <- specData "Inverted" "specification/inverted.json" s5 <- specData "Partials" "specification/partials.json" s6 <- specData "Sections" "specification/sections.json" return $ s1 >> s2 >> s3 >> s4 >> s5 >> s6 specData :: String -> FilePath -> IO Spec specData aspect name = do bytes <- BSL.readFile name return (specData' bytes) where specData' bytes = describe aspect $ do let handleError = expectationFailure . show case eitherDecode bytes of Left err -> it "should load YAML specs first" $ expectationFailure (show err) Right SpecFile {..} -> forM_ specTests $ \Test {..} -> it (testName ++ ": " ++ testDesc) $ case compileMustacheText (PName $ T.pack testName) testTemplate of Left perr -> handleError perr Right Template {..} -> do ps1 <- forM (M.keys testPartials) $ \k -> do let pname = PName k case parseMustache (T.unpack k) (testPartials ! k) of Left perr -> handleError perr >> undefined Right ns -> return (pname, ns) let ps2 = M.fromList ps1 `M.union` templateCache let (_ws, t) = renderMustacheW (Template templateActual ps2) testData -- _ <- traverse print _ws t `shouldBe` testExpected microstache-1.0.2.3/specification/0000755000000000000000000000000007346545000015165 5ustar0000000000000000microstache-1.0.2.3/specification/comments.json0000644000000000000000000000442607346545000017713 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]}microstache-1.0.2.3/specification/delimiters.json0000644000000000000000000000652707346545000020233 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes. .yes. ]\n[ .yes. .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"| |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":" {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @ @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]}microstache-1.0.2.3/specification/interpolation.json0000644000000000000000000001706407346545000020757 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag. A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object, the data is the value returned by the\n method with the given name.\n 5) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: & " < >\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}microstache-1.0.2.3/specification/inverted.json0000644000000000000000000001407307346545000017705 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}microstache-1.0.2.3/specification/partials.json0000644000000000000000000000603707346545000017705 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]}microstache-1.0.2.3/specification/sections.json0000644000000000000000000001717407346545000017721 0ustar0000000000000000{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}microstache-1.0.2.3/src/Text/0000755000000000000000000000000007346545000014060 5ustar0000000000000000microstache-1.0.2.3/src/Text/Microstache.hs0000644000000000000000000000577107346545000016667 0ustar0000000000000000-- | -- Module : Text.Microstache -- Copyright : © 2016–2017 Stack Builders -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- This is a Haskell implementation of Mustache templates. The -- implementation conforms to the version 1.1.3 of official Mustache -- specification . It is extremely simple -- and straightforward to use with minimal but complete API — three -- functions to compile templates (from directory, from file, and from lazy -- text) and one to render them. -- -- For rendering you only need to create Aeson's 'Data.Aeson.Value' where -- you put the data to interpolate. Since the library re-uses Aeson's -- instances and most data types in Haskell ecosystem are instances of -- classes like 'Data.Aeson.ToJSON', the whole process is very simple for -- the end user. -- -- Template Haskell helpers for compilation of templates at compile time are -- available in the "Text.Microstache.Compile.TH" module. The helpers are -- currently available only for GHC 8 users though. -- -- One feature that is not currently supported is lambdas. The feature is -- marked as optional in the spec and can be emulated via processing of -- parsed template representation. The decision to drop lambdas is -- intentional, for the sake of simplicity and better integration with -- Aeson. -- -- Here is an example of basic usage: -- -- > {-# LANGUAGE OverloadedStrings #-} -- > -- > module Main (main) where -- > -- > import Data.Aeson -- > import Data.Text -- > import Text.Microstache -- > import qualified Data.Text.Lazy.IO as TIO -- > -- > main :: IO () -- > main = do -- > let res = compileMustacheText "foo" -- > "Hi, {{name}}! You have:\n{{#things}}\n * {{.}}\n{{/things}}\n" -- > case res of -- > Left err -> print err -- > Right template -> TIO.putStr $ renderMustache template $ object -- > [ "name" .= ("John" :: Text) -- > , "things" .= ["pen" :: Text, "candle", "egg"] -- > ] -- -- If I run the program, it prints the following: -- -- > Hi, John! You have: -- > * pen -- > * candle -- > * egg -- -- For more information about Mustache templates the following links may be -- helpful: -- -- * The official Mustache site: -- * The manual: -- * The specification: -- * Stack Builders Stache tutorial: module Text.Microstache ( -- * Types Template (..) , Node (..) , Key (..) , PName (..) , MustacheException (..) , displayMustacheException , MustacheWarning (..) , displayMustacheWarning -- * Compiling , compileMustacheDir , compileMustacheFile , compileMustacheText -- * Rendering , renderMustache , renderMustacheW ) where import Text.Microstache.Compile import Text.Microstache.Render import Text.Microstache.Type microstache-1.0.2.3/src/Text/Microstache/0000755000000000000000000000000007346545000016321 5ustar0000000000000000microstache-1.0.2.3/src/Text/Microstache/Compile.hs0000644000000000000000000001055507346545000020253 0ustar0000000000000000-- | -- Module : Text.Microstache.Compile -- Copyright : © 2016–2017 Stack Builders -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Mustache 'Template' creation from file or a 'Text' value. You don't -- usually need to import the module, because "Text.Microstache" re-exports -- everything you may need, import that module instead. {-# LANGUAGE CPP #-} module Text.Microstache.Compile ( compileMustacheDir , getMustacheFilesInDir , compileMustacheFile , compileMustacheText ) where import Control.Exception (throwIO) import Control.Monad (filterM, foldM) import Data.Text.Lazy (Text) import System.Directory (doesFileExist, getCurrentDirectory, getDirectoryContents) import Text.Parsec (ParseError) import qualified Data.Map as Map import qualified Data.Text as T import qualified Data.Text.Lazy.IO as LT import qualified System.FilePath as F #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) #endif import Text.Microstache.Parser import Text.Microstache.Type -- | Compile all templates in specified directory and select one. Template -- files should have extension @mustache@, (e.g. @foo.mustache@) to be -- recognized. This function /does not/ scan the directory recursively. -- -- The action can throw the same exceptions as 'getDirectoryContents', and -- 'T.readFile'. compileMustacheDir :: PName -- ^ Which template to select after compiling -> FilePath -- ^ Directory with templates -> IO Template -- ^ The resulting template compileMustacheDir pname path = getMustacheFilesInDir path >>= fmap selectKey . foldM f (Template undefined Map.empty) where selectKey t = t { templateActual = pname } f (Template _ old) fp = do Template _ new <- compileMustacheFile fp return (Template undefined (Map.union new old)) -- | Return a list of templates found in given directory. The returned paths -- are absolute. getMustacheFilesInDir :: FilePath -- ^ Directory with templates -> IO [FilePath] getMustacheFilesInDir path = getDirectoryContents path >>= filterM isMustacheFile . fmap (F.combine path) >>= mapM makeAbsolute' -- | Compile single Mustache template and select it. -- -- The action can throw the same exceptions as 'T.readFile'. compileMustacheFile :: FilePath -- ^ Location of the file -> IO Template compileMustacheFile path = LT.readFile path >>= withException . compile where pname = pathToPName path compile = fmap (Template pname . Map.singleton pname) . parseMustache path -- | Compile Mustache template from a lazy 'Text' value. The cache will -- contain only this template named according to given 'PName'. compileMustacheText :: PName -- ^ How to name the template? -> Text -- ^ The template to compile -> Either ParseError Template -- ^ The result compileMustacheText pname txt = Template pname . Map.singleton pname <$> parseMustache "" txt ---------------------------------------------------------------------------- -- Helpers -- | Check if given 'FilePath' points to a mustache file. isMustacheFile :: FilePath -> IO Bool isMustacheFile path = do exists <- doesFileExist path let rightExtension = F.takeExtension path == ".mustache" return (exists && rightExtension) -- | Build a 'PName' from given 'FilePath'. pathToPName :: FilePath -> PName pathToPName = PName . T.pack . F.takeBaseName -- | Throw 'MustacheException' if argument is 'Left' or return the result -- inside 'Right'. withException :: Either ParseError Template -- ^ Value to process -> IO Template -- ^ The result withException = either (throwIO . MustacheParserException) return makeAbsolute' :: FilePath -> IO FilePath makeAbsolute' path0 = fmap (matchTrailingSeparator path0 . F.normalise) (prependCurrentDirectory path0) where prependCurrentDirectory :: FilePath -> IO FilePath prependCurrentDirectory path = if F.isRelative path -- avoid the call to `getCurrentDirectory` if we can then (F. path) <$> getCurrentDirectory else return path matchTrailingSeparator :: FilePath -> FilePath -> FilePath matchTrailingSeparator path | F.hasTrailingPathSeparator path = F.addTrailingPathSeparator | otherwise = F.dropTrailingPathSeparator microstache-1.0.2.3/src/Text/Microstache/Parser.hs0000644000000000000000000001461007346545000020113 0ustar0000000000000000-- | -- Module : Text.Microstache.Parser -- Copyright : © 2016–2017 Stack Builders -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Megaparsec parser for Mustache templates. You don't usually need to -- import the module, because "Text.Microstache" re-exports everything you may -- need, import that module instead. {-# LANGUAGE CPP #-} module Text.Microstache.Parser ( parseMustache ) where import Control.Applicative (Alternative (..), (<$), (<$>)) import Control.Monad (unless, void) import Data.Char (isAlphaNum, isSpace) import Data.Functor.Identity (Identity, runIdentity) import Data.List (intercalate) import Data.Maybe (catMaybes) import Data.Text.Lazy (Text) import Data.Word (Word) import Text.Parsec (ParseError, ParsecT, Stream, anyChar, between, char, choice, eof, getPosition, getState, label, lookAhead, manyTill, notFollowedBy, oneOf, putState, runParserT, satisfy, sepBy1, sourceColumn, spaces, string, try, ()) import Text.Parsec.Char () import Text.Microstache.Type import qualified Data.Text as T #if !MIN_VERSION_base(4,8,0) import Control.Applicative (Applicative (..)) #endif ---------------------------------------------------------------------------- -- Parser -- | Parse given Mustache template. parseMustache :: FilePath -- ^ Location of file to parse -> Text -- ^ File contents (Mustache template) -> Either ParseError [Node] -- ^ Parsed nodes or parse error parseMustache name contents = runIdentity (runParserT (pMustache eof) (Delimiters "{{" "}}") name contents) pMustache :: Parser () -> Parser [Node] pMustache = fmap catMaybes . manyTill (choice alts) where alts = [ Nothing <$ withStandalone pComment , Just <$> pSection "#" Section , Just <$> pSection "^" InvertedSection , Just <$> pStandalone (pPartial Just) , Just <$> pPartial (const Nothing) , Nothing <$ withStandalone pSetDelimiters , Just <$> pUnescapedVariable , Just <$> pUnescapedSpecial , Just <$> pEscapedVariable , Just <$> pTextBlock ] {-# INLINE pMustache #-} pTextBlock :: Parser Node pTextBlock = do start <- gets openingDel (void . notFollowedBy . string') start let terminator = choice [ (void . lookAhead . string') start , pBol , eof ] TextBlock . T.pack <$> someTill anyChar terminator {-# INLINE pTextBlock #-} pUnescapedVariable :: Parser Node pUnescapedVariable = UnescapedVar <$> pTag "&" {-# INLINE pUnescapedVariable #-} pUnescapedSpecial :: Parser Node pUnescapedSpecial = do start <- gets openingDel end <- gets closingDel between (symbol $ start ++ "{") (string $ "}" ++ end) $ UnescapedVar <$> pKey {-# INLINE pUnescapedSpecial #-} pSection :: String -> (Key -> [Node] -> Node) -> Parser Node pSection suffix f = do key <- withStandalone (pTag suffix) nodes <- (pMustache . withStandalone . pClosingTag) key return (f key nodes) {-# INLINE pSection #-} pPartial :: (Word -> Maybe Word) -> Parser Node pPartial f = do pos <- f <$> indentLevel key <- pTag ">" let pname = PName $ T.intercalate (T.pack ".") (unKey key) return (Partial pname pos) {-# INLINE pPartial #-} pComment :: Parser () pComment = void $ do start <- gets openingDel end <- gets closingDel (void . symbol) (start ++ "!") manyTill anyChar (string end) {-# INLINE pComment #-} pSetDelimiters :: Parser () pSetDelimiters = void $ do start <- gets openingDel end <- gets closingDel (void . symbol) (start ++ "=") start' <- pDelimiter <* scn end' <- pDelimiter <* scn (void . string) ("=" ++ end) putState (Delimiters start' end') {-# INLINE pSetDelimiters #-} pEscapedVariable :: Parser Node pEscapedVariable = EscapedVar <$> pTag "" {-# INLINE pEscapedVariable #-} withStandalone :: Parser a -> Parser a withStandalone p = pStandalone p <|> p {-# INLINE withStandalone #-} pStandalone :: Parser a -> Parser a pStandalone p = pBol *> try (between sc (sc <* (void eol <|> eof)) p) {-# INLINE pStandalone #-} pTag :: String -> Parser Key pTag suffix = do start <- gets openingDel end <- gets closingDel between (symbol $ start ++ suffix) (string end) pKey {-# INLINE pTag #-} pClosingTag :: Key -> Parser () pClosingTag key = do start <- gets openingDel end <- gets closingDel let str = keyToString key void $ between (symbol $ start ++ "/") (string end) (symbol str) {-# INLINE pClosingTag #-} pKey :: Parser Key pKey = (fmap Key . lexeme . flip label "key") (implicit <|> other) where implicit = [] <$ char '.' other = sepBy1 (T.pack <$> some ch) (char '.') ch = alphaNumChar <|> oneOf "-_" {-# INLINE pKey #-} pDelimiter :: Parser String pDelimiter = some (satisfy delChar) "delimiter" where delChar x = not (isSpace x) && x /= '=' {-# INLINE pDelimiter #-} indentLevel :: Parser Word indentLevel = fmap (fromIntegral . sourceColumn) getPosition pBol :: Parser () pBol = do level <- indentLevel unless (level == 1) empty {-# INLINE pBol #-} ---------------------------------------------------------------------------- -- Auxiliary types -- | Type of Mustache parser monad stack. type Parser = ParsecT Text Delimiters Identity -- | State used in Mustache parser. It includes currently set opening and -- closing delimiters. data Delimiters = Delimiters { openingDel :: String , closingDel :: String } ---------------------------------------------------------------------------- -- Lexer helpers and other -- TODO: OLEG inline scn :: Parser () scn = spaces {-# INLINE scn #-} sc :: Parser () sc = void (many (oneOf " \t")) {-# INLINE sc #-} lexeme :: Parser a -> Parser a lexeme p = p <* spaces {-# INLINE lexeme #-} eol :: Parser () eol = void (char '\n') <|> void (char '\r' >> char '\n') string' :: String -> Parser String string' = try . string symbol :: String -> Parser String symbol = lexeme . string' {-# INLINE symbol #-} keyToString :: Key -> String keyToString (Key []) = "." keyToString (Key ks) = intercalate "." (T.unpack <$> ks) {-# INLINE keyToString #-} someTill :: Stream s m t => ParsecT s u m a -> ParsecT s u m end -> ParsecT s u m [a] someTill p end = (:) <$> p <*> manyTill p end gets :: Monad m => (u -> a) -> ParsecT s u m a gets f = fmap f getState alphaNumChar :: Parser Char alphaNumChar = satisfy isAlphaNum microstache-1.0.2.3/src/Text/Microstache/Render.hs0000644000000000000000000002371007346545000020077 0ustar0000000000000000-- | -- Module : Text.Microstache.Render -- Copyright : © 2016–2017 Stack Builders -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Functions for rendering Mustache templates. You don't usually need to -- import the module, because "Text.Microstache" re-exports everything you may -- need, import that module instead. {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Text.Microstache.Render ( renderMustache, renderMustacheW ) where import Control.Monad (forM_, unless, when) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (ReaderT (..), asks, local) import Data.Aeson (Value (..), encode) import Data.Foldable (asum) import Data.List (tails) import Data.List.NonEmpty (NonEmpty (..)) import Data.Monoid (mempty) import Data.Semigroup ((<>)) import Data.Text (Text) import Data.Word (Word) import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import qualified Data.Text as T import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.Builder as B import qualified Data.Text.Lazy.Encoding as LTE import qualified Data.Vector as V #if MIN_VERSION_aeson(2,0,0) import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.Key as Key #else import qualified Data.HashMap.Strict as KM #endif #if MIN_VERSION_transformers(0,4,0) import Control.Monad.Trans.State.Strict (State, execState, modify') #else import Control.Monad.Trans.State.Strict (State, execState, get, put) #endif #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) #endif import Text.Microstache.Type #if !(MIN_VERSION_transformers(0,4,0)) modify' :: (s -> s) -> State s () modify' f = do s <- get put $! f s #endif ---------------------------------------------------------------------------- -- The rendering monad -- | Synonym for the monad we use for rendering. It allows to share context -- and accumulate the result as 'B.Builder' data which is then turned into -- lazy 'LT.Text'. type Render a = ReaderT RenderContext (State S) a 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 , rcContext :: NonEmpty Value -- ^ The context stack , rcPrefix :: Key -- ^ Prefix accumulated by entering sections , rcTemplate :: Template -- ^ The template to render , rcLastNode :: Bool -- ^ Is this last node in this partial? } ---------------------------------------------------------------------------- -- High-level interface -- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values -- for interpolation. renderMustache :: Template -> Value -> LT.Text renderMustache t = snd . renderMustacheW t -- | Like 'renderMustache' but also return a list of warnings. -- -- @since 1.0.1 renderMustacheW :: Template -> Value -> ([MustacheWarning], LT.Text) renderMustacheW t = runRender (renderPartial (templateActual t) Nothing renderNode) t -- | Render a single 'Node'. renderNode :: Node -> Render () renderNode (TextBlock txt) = outputIndented txt renderNode (EscapedVar k) = lookupKey k >>= renderValue k >>= outputRaw . escapeHtml renderNode (UnescapedVar k) = lookupKey k >>= renderValue k >>= outputRaw renderNode (Section k ns) = do val <- lookupKey k enterSection k $ unless (isBlank val) $ case val of Array xs -> forM_ (V.toList xs) $ \x -> addToLocalContext x (renderMany renderNode ns) _ -> addToLocalContext val (renderMany renderNode ns) renderNode (InvertedSection k ns) = do val <- lookupKey k when (isBlank val) $ renderMany renderNode ns renderNode (Partial pname indent) = renderPartial pname indent renderNode ---------------------------------------------------------------------------- -- The rendering monad vocabulary -- | Run 'Render' monad given template to render and a 'Value' to take -- values from. runRender :: Render a -> Template -> Value -> ([MustacheWarning], LT.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 } {-# 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 :: PName -- ^ Name of partial to render -> Maybe Word -- ^ Indentation level to use -> (Node -> Render ()) -- ^ How to render nodes in that partial -> 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 (Map.findWithDefault [] actual cache) {-# INLINE getNodes #-} -- | Render many nodes. renderMany :: (Node -> Render ()) -- ^ How to render a node -> [Node] -- ^ The collection of nodes to render -> 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 -- | Lookup a 'Value' by its 'Key'. lookupKey :: Key -> Render Value lookupKey (Key []) = NE.head <$> asks rcContext 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 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 -- “path”. simpleLookup :: Bool -- ^ At least one part of the path matched, in this case we are -- “committed” to this lookup and cannot say “there is nothing, try -- other level”. This is necessary to pass the “Dotted Names — Context -- Precedence” test from the “interpolation.yml” spec. -> Key -- ^ The key to lookup -> Value -- ^ Source value -> Maybe Value -- ^ Looked-up value simpleLookup _ (Key []) obj = return obj simpleLookup c (Key (k:ks)) (Object m) = #if MIN_VERSION_aeson(2,0,0) case KM.lookup (Key.fromText k) m of #else case KM.lookup k m of #endif 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 -- | Add two 'Maybe' 'Word' values together. addIndents :: Maybe Word -> Maybe Word -> Maybe Word addIndents Nothing Nothing = Nothing addIndents Nothing (Just x) = Just x addIndents (Just x) Nothing = Just x addIndents (Just x) (Just y) = Just (x + y) {-# INLINE addIndents #-} -- | Build intentation of specified length by repeating the space character. buildIndent :: Maybe Word -> Text buildIndent Nothing = "" buildIndent (Just p) = let n = fromIntegral p - 1 in T.replicate n " " {-# INLINE buildIndent #-} -- | Select invisible values. isBlank :: Value -> Bool isBlank Null = True isBlank (Bool False) = True isBlank (Object m) = KM.null m 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 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 . LT.toStrict . LTE.decodeUtf8 . encode {-# INLINE renderValue #-} -- | Escape HTML represented as strict 'Text'. escapeHtml :: Text -> Text escapeHtml txt = foldr (uncurry T.replace) txt [ ("\"", """) , ("<", "<") , (">", ">") , ("&", "&") ] {-# INLINE escapeHtml #-} microstache-1.0.2.3/src/Text/Microstache/Type.hs0000644000000000000000000001270607346545000017604 0ustar0000000000000000-- | -- Module : Text.Microstache.Type -- Copyright : © 2016–2017 Stack Buliders -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Types used by the package. You don't usually need to import the module, -- because "Text.Microstache" re-exports everything you may need, import that -- module instead. {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} module Text.Microstache.Type ( Template (..) , Node (..) , Key (..) , showKey , PName (..) , MustacheException (..) , displayMustacheException , MustacheWarning (..) , displayMustacheWarning ) where import Control.DeepSeq (NFData (..)) import Control.Exception (Exception (..)) import Data.Data (Data) import Data.Map (Map) import Data.Monoid (Monoid (..)) import Data.Semigroup (Semigroup (..)) import Data.String (IsString (..)) import Data.Text (Text) import Data.Typeable (Typeable) import Data.Word (Word) import GHC.Generics import Text.Parsec (ParseError) import qualified Data.Map as Map import qualified Data.Text as T -- | Mustache template as name of “top-level” template and a collection of -- all available templates (partials). -- -- 'Template' is a 'Semigroup'. This means that you can combine 'Template's -- (and their caches) using the @('<>')@ operator, the resulting 'Template' -- will have the same currently selected template as the left one. Union of -- caches is also left-biased. data Template = Template { templateActual :: PName -- ^ Name of currently “selected” template (top-level one). , templateCache :: Map PName [Node] -- ^ Collection of all templates that are available for interpolation -- (as partials). The top-level one is also contained here and the -- “focus” can be switched easily by modifying 'templateActual'. } deriving (Eq, Ord, Show, Data, Typeable, Generic) instance Semigroup Template where (Template pname x) <> (Template _ y) = Template pname (Map.union x y) -- | Structural element of template. data Node = TextBlock Text -- ^ Plain text contained between tags | EscapedVar Key -- ^ HTML-escaped variable | UnescapedVar Key -- ^ Unescaped variable | Section Key [Node] -- ^ Mustache section | InvertedSection Key [Node] -- ^ Inverted section | Partial PName (Maybe Word) -- ^ Partial with indentation level ('Nothing' means it was inlined) deriving (Eq, Ord, Show, Data, Typeable, Generic) -- | Identifier for values to interpolate. -- -- The representation is the following: -- -- * @[]@ — empty list means implicit iterators; -- * @[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) instance NFData Key -- | Pretty-print a key, this is helpful, for example, if you want to -- display an error message. showKey :: Key -> Text showKey (Key []) = "" showKey (Key xs) = T.intercalate "." xs -- | 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) instance IsString PName where fromString = PName . T.pack instance NFData PName -- | Exception that is thrown when parsing of a template has failed or -- referenced values were not provided. data MustacheException = MustacheParserException ParseError -- ^ Template parser has failed. This contains the parse error. | 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) {-# 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) 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 microstache-1.0.2.3/tasty-as-hspec/Test/0000755000000000000000000000000007346545000016131 5ustar0000000000000000microstache-1.0.2.3/tasty-as-hspec/Test/Hspec.hs0000644000000000000000000000620207346545000017527 0ustar0000000000000000{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeOperators #-} -- | A @hspec@ like interface build on top of tasty. module Test.Hspec ( -- * Runner Spec, hspec, -- * Test trees describe, context, it, -- * Checks Expectation, expectationFailure, shouldBe, shouldContain, shouldNotContain, compareWith, ) where import Control.Applicative (Applicative) import Control.Monad (unless) import Data.List (isInfixOf) import Data.Orphans () import Prelude (Bool, Eq, Functor, IO, Monad, Show, String, flip, fst, id, not, show, ($), (++)) import Test.Tasty (TestName, TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (Assertion, HasCallStack, assertFailure, testCase, (@?=)) ------------------------------------------------------------------------------- -- Runner ------------------------------------------------------------------------------- type Spec = TestTreeM () hspec :: TestTreeM () -> IO () hspec t = defaultMain (testGroup "X" (runTestTreeM t)) ------------------------------------------------------------------------------- -- Test trees ------------------------------------------------------------------------------- newtype TestTreeM a = TestTreeM (Writer [TestTree] a) deriving (Functor, Applicative, Monad) runTestTreeM :: TestTreeM () -> [TestTree] runTestTreeM (TestTreeM m) = fst (runWriter m) class Describe r where describe :: TestName -> TestTreeM () -> r instance a ~ () => Describe (TestTreeM a) where describe n t = TestTreeM $ tell [ describe n t ] instance Describe TestTree where describe n t = testGroup n $ runTestTreeM t it :: TestName -> Assertion -> TestTreeM () it n assertion = TestTreeM $ tell [ testCase n assertion ] context :: Describe r => TestName -> TestTreeM () -> r context n t = describe n t ------------------------------------------------------------------------------- -- Checks ------------------------------------------------------------------------------- type Expectation = Assertion expectationFailure :: String -> Expectation expectationFailure = assertFailure shouldBe :: (Eq a, Show a, HasCallStack) => a -> a -> Expectation shouldBe = (@?=) shouldContain :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Expectation shouldContain = compareWith (flip isInfixOf) "does not contain" shouldNotContain :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Expectation shouldNotContain = compareWith (\x y -> not (isInfixOf y x)) "contains" compareWith :: (Show a, Show b, HasCallStack) => (a -> b -> Bool) -> String -> a -> b -> Expectation compareWith f msg x y = unless (f x y) $ assertFailure $ show x ++ " " ++ msg ++ " " ++ show y ------------------------------------------------------------------------------- -- Writer ------------------------------------------------------------------------------- type Writer = (,) runWriter :: Writer w a -> (w, a) runWriter = id tell :: w -> (w, ()) tell w = (w, ()) microstache-1.0.2.3/tests/0000755000000000000000000000000007346545000013507 5ustar0000000000000000microstache-1.0.2.3/tests/Spec.hs0000644000000000000000000000040307346545000014732 0ustar0000000000000000module Main (main) where import Test.Hspec import qualified Text.Microstache.ParserSpec as P import qualified Text.Microstache.RenderSpec as R import qualified Text.Microstache.TypeSpec as T main :: IO () main = hspec $ do P.spec R.spec T.spec microstache-1.0.2.3/tests/Text/Microstache/0000755000000000000000000000000007346545000016674 5ustar0000000000000000microstache-1.0.2.3/tests/Text/Microstache/ParserSpec.hs0000644000000000000000000001075507346545000021307 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Text.Microstache.ParserSpec ( main , spec ) where import Control.Monad (unless) import Data.List.NonEmpty (NonEmpty (..)) import Test.Hspec import Text.Parsec import Text.Microstache.Parser import Text.Microstache.Type import qualified Data.List.NonEmpty as NE import qualified Data.Set as S #if !MIN_VERSION_base(4,8,0) import Control.Applicative (pure) #endif main :: IO () main = hspec spec spec :: Spec spec = describe "parseMustache" $ do let p = parseMustache "" key = Key . pure it "parses text" $ p "test12356p0--=-34{}jnv,\n" `shouldParse` [TextBlock "test12356p0--=-34{}jnv,\n"] context "when parsing a variable" $ do context "with white space" $ do it "parses escaped {{ variable }}" $ p "{{ name }}" `shouldParse` [EscapedVar (key "name")] it "parses unescaped {{{ variable }}}" $ p "{{{ name }}}" `shouldParse` [UnescapedVar (key "name")] it "parses unescaped {{& variable }}" $ p "{{& name }}" `shouldParse` [UnescapedVar (key "name")] context "without white space" $ do it "parses escaped {{variable}}" $ p "{{name}}" `shouldParse` [EscapedVar (key "name")] it "parses unescaped {{{variable}}}" $ p "{{{name}}}" `shouldParse` [UnescapedVar (key "name")] it "parses unescaped {{& variable }}" $ p "{{&name}}" `shouldParse` [UnescapedVar (key "name")] it "allows '-' in variable names" $ p "{{ var-name }}" `shouldParse` [EscapedVar (key "var-name")] it "allows '_' in variable names" $ p "{{ var_name }}" `shouldParse` [EscapedVar (key "var_name")] context "when parsing a section" $ do it "parses empty section" $ p "{{#section}}{{/section}}" `shouldParse` [Section (key "section") []] it "parses non-empty section" $ p "{{# section }}Hi, {{name}}!\n{{/section}}" `shouldParse` [Section (key "section") [ TextBlock "Hi, " , EscapedVar (key "name") , TextBlock "!\n"]] context "when parsing an inverted section" $ do it "parses empty inverted section" $ p "{{^section}}{{/section}}" `shouldParse` [InvertedSection (key "section") []] it "parses non-empty inverted section" $ p "{{^ section }}No one here?!\n{{/section}}" `shouldParse` [InvertedSection (key "section") [TextBlock "No one here?!\n"]] context "when parsing a partial" $ do it "parses a partial with white space" $ p "{{> that-s_my-partial }}" `shouldParse` [Partial "that-s_my-partial" (Just 1)] it "parses a partial without white space" $ p "{{>that-s_my-partial}}" `shouldParse` [Partial "that-s_my-partial" (Just 1)] it "handles indented partial correctly" $ p " {{> next_one }}" `shouldParse` [Partial "next_one" (Just 4)] context "when running into delimiter change" $ do it "has effect" $ p "{{=<< >>=}}<>{{var}}" `shouldParse` [EscapedVar (key "var"), TextBlock "{{var}}"] it "handles whitespace just as well" $ p "{{=<< >>=}}<< var >>{{ var }}" `shouldParse` [EscapedVar (key "var"), TextBlock "{{ var }}"] it "affects {{{s" $ p "{{=<< >>=}}<<{var}>>" `shouldParse` [UnescapedVar (key "var")] it "parses two subsequent delimiter changes" $ p "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-" `shouldParse` [EscapedVar (key "var"), Section (key "section") []] it "propagates delimiter change from a nested scope" $ p "{{#section}}{{=<< >>=}}<><>" `shouldParse` [Section (key "section") [], EscapedVar (key "var")] context "when given malformed input" $ do it "rejects unclosed tags" $ shouldFailParse $ p "{{ name" it "rejects unknown tags" $ shouldFailParse $ p "{{? boo }}" ------------------------------------------------------------------------------- -- Tools ------------------------------------------------------------------------------- shouldParse :: (Eq a, Show a) => Either ParseError a -> a -> Expectation shouldParse (Left e) v = expectationFailure $ "expected: " ++ show v ++ "\nbut parsing failed with error:\n" ++ show e shouldParse (Right x) v = unless (x == v) $ expectationFailure $ "expected: " ++ show v ++ "\nbut got: " ++ show x shouldFailParse :: (Eq a, Show a) => Either ParseError a -> Expectation shouldFailParse (Left _) = pure () shoulwFailParse (Right x) = expectationFailure $ "expected parse failure, got " ++ show x microstache-1.0.2.3/tests/Text/Microstache/RenderSpec.hs0000644000000000000000000001516707346545000021274 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Text.Microstache.RenderSpec ( main , spec ) where import Control.Exception (evaluate) import Data.Aeson (object, KeyValue (..), Value (..)) import Data.Text (Text) import Test.Hspec import Text.Parsec import Text.Microstache.Render import Text.Microstache.Type import qualified Data.Map as M #if !MIN_VERSION_base(4,8,0) import Control.Applicative (pure) #endif main :: IO () main = hspec spec spec :: Spec spec = describe "renderMustache" $ do 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 . return it "leaves text block “as is”" $ r [TextBlock "a text block"] Null `shouldBe` "a text block" it "renders escaped variables correctly" $ r [EscapedVar (key "foo")] (object ["foo" .= ("&\"something\"" :: Text)]) `shouldBe` "<html>&"something"</html>" it "renders unescaped variables “as is”" $ r [UnescapedVar (key "foo")] (object ["foo" .= ("&\"something\"" :: Text)]) `shouldBe` "&\"something\"" 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 "*"]] context "when the key is not present" $ it "warns with the correct warning" $ w nodes (object []) `shouldBe` [MustacheVariableNotFound (key "foo")] context "when the key is not present inside a section" $ 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" $ r nodes (object ["foo" .= Null]) `shouldBe` "" it "skips false Boolean" $ r nodes (object ["foo" .= False]) `shouldBe` "" it "skips empty list" $ r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` "" it "skips empty object" $ r nodes (object ["foo" .= object []]) `shouldBe` "" it "skips empty string" $ r nodes (object ["foo" .= ("" :: Text)]) `shouldBe` "" context "when the key is a Boolean true" $ it "renders the section without interpolation" $ r [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= object ["bar" .= True]]) `shouldBe` "brr" context "when the key is an object" $ it "uses it to render section once" $ r nodes (object ["foo" .= object ["bar" .= ("huh?" :: Text)]]) `shouldBe` "huh?*" context "when the key is a singleton list" $ it "uses it to render section once" $ r nodes (object ["foo" .= object ["bar" .= ("huh!" :: Text)]]) `shouldBe` "huh!*" context "when the key is a list of Boolean trues" $ it "renders the section as many times as there are elements" $ r [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= [True, True]]) `shouldBe` "brrbrr" context "when the key is a list of objects" $ it "renders the section many times changing context" $ r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1..4] :: [Int]]]) `shouldBe` "1*2*3*4*" context "when the key is a number" $ do it "renders the section" $ r [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= (5 :: Int)]) `shouldBe` "brr" it "uses the key as context" $ r [Section (key "foo") [EscapedVar (Key [])]] (object ["foo" .= (5 :: Int)]) `shouldBe` "5" context "when the key is a non-empty string" $ do it "renders the section" $ r [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= ("x" :: Text)]) `shouldBe` "brr" it "uses the key as context" $ r [Section (key "foo") [EscapedVar (Key [])]] (object ["foo" .= ("x" :: Text)]) `shouldBe` "x" context "when rendering an inverted section" $ do let nodes = [InvertedSection (key "foo") [TextBlock "Here!"]] context "when the key is not present" $ 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" $ r nodes (object ["foo" .= Null]) `shouldBe` "Here!" it "renders with false Boolean" $ r nodes (object ["foo" .= False]) `shouldBe` "Here!" it "renders with empty list" $ r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` "Here!" it "renders with empty object" $ r nodes (object ["foo" .= object []]) `shouldBe` "Here!" context "when the key is a “true” value" $ do it "skips true Boolean" $ r nodes (object ["foo" .= True]) `shouldBe` "" it "skips non-empty object" $ r nodes (object ["foo" .= object ["bar" .= True]]) `shouldBe` "" it "skips non-empty list" $ r nodes (object ["foo" .= [True]]) `shouldBe` "" context "when rendering a partial" $ do let nodes = [ Partial "partial" (Just 4) , TextBlock "*" ] it "skips missing partial" $ r nodes Null `shouldBe` " *" it "renders partial correctly" $ let template = Template "test" $ M.fromList [ ("test", nodes) , ("partial", [TextBlock "one\ntwo\nthree"]) ] in renderMustache template Null `shouldBe` " one\n two\n three*" context "when using dotted keys inside a section" $ it "it should be equivalent to access via one more section" $ r [ Section (key "things") [ EscapedVar (Key ["atts", "color"]) , TextBlock " == " , Section (key "atts") [EscapedVar (key "color")] ] ] (object ["things" .= [object ["atts" .= object ["color" .= ("blue" :: Text)]]]]) `shouldBe` "blue == blue" microstache-1.0.2.3/tests/Text/Microstache/TypeSpec.hs0000644000000000000000000000261507346545000020770 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Text.Microstache.TypeSpec ( main , spec ) where import Data.Semigroup ((<>)) import Test.Hspec import Text.Microstache.Type import qualified Data.Map as M main :: IO () main = hspec spec spec :: Spec spec = do describe "Template instances" $ context "the Semigroup instance" $ do it "the resulting template inherits focus of the left one" $ templateActual (templateA <> templateB) `shouldBe` templateActual templateA it "the resulting template merges caches with left bias" $ templateCache (templateA <> templateB) `shouldBe` M.fromList [ ("c", [TextBlock "foo"]) , ("d", [TextBlock "bar"]) , ("e", [TextBlock "baz"]) ] describe "showKey" $ do context "when the key has no elements in it" $ it "is rendered correctly" $ showKey (Key []) `shouldBe` "" context "when the key has some elements" $ it "is rendered correctly" $ do showKey (Key ["boo"]) `shouldBe` "boo" showKey (Key ["foo","bar"]) `shouldBe` "foo.bar" showKey (Key ["baz","baz","quux"]) `shouldBe` "baz.baz.quux" templateA :: Template templateA = Template "a" $ M.fromList [ ("c", [TextBlock "foo"]) , ("d", [TextBlock "bar"]) ] templateB :: Template templateB = Template "b" $ M.fromList [ ("c", [TextBlock "bar"]) , ("e", [TextBlock "baz"]) ]