packages feed

ede 0.3.2.0 → 0.3.4.0

raw patch · 21 files changed

Files

app/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} @@ -5,23 +6,29 @@  import qualified Control.Monad.Fail as Fail import qualified Data.Aeson as Aeson+#ifdef VERSION_attoparsec_aeson+import qualified Data.Aeson.Parser as Aeson+#endif import qualified Data.Attoparsec.ByteString as Parsec import qualified Data.ByteString as ByteString import qualified Data.ByteString.Lazy as ByteString.Lazy+import Data.HashMap.Strict (HashMap)+import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text.Encoding import qualified Data.Text.Lazy.IO as Text.Lazy.IO import qualified Options.Applicative as Options import qualified Options.Applicative.Help.Pretty as Pretty-import qualified System.Exit as Exit-import qualified Text.EDE as EDE import qualified Prettyprinter as PP import qualified Prettyprinter.Render.Terminal as PP+import qualified System.Exit as Exit+import qualified Text.EDE as EDE+import qualified Text.EDE.Internal.Compat as Compat  data Options = Options   { templateFile :: FilePath,     jsonFile :: Maybe FilePath,-    jsonObject :: Maybe Aeson.Object+    jsonObject :: Maybe (HashMap Text Aeson.Value)   }  optionsParser :: Options.Parser Options@@ -38,8 +45,8 @@           ( Options.long "context-file"               <> Options.metavar "PATH"               <> Options.help-                "Path of a file containing a JSON object which is used \-                \as the template context"+                ("Path of a file containing a JSON object which is used " <>+                 "as the template context")           )       )     <*> Options.optional@@ -60,26 +67,32 @@   where     usage =       Pretty.vcat-        [ Pretty.empty,+        [ Pretty.emptyDoc,           "The --context-file and --context-json options are processed as follows:",           Pretty.indent 2 "1.)"             Pretty.<+> ( Pretty.align-                           ( "Both are provided"-                               Pretty.<$$> "Both objects are merged into one with the keys of \-                                           \--context-json taking precedence over those in the \-                                           \file provided by --context-file."+                           ( Pretty.vcat+                               [ "Both are provided",+                                 "Both objects are merged into one with the keys of " <>+                                 "--context-json taking precedence over those in the " <>+                                 "file provided by --context-file."+                               ]                            )                        ),           Pretty.indent 2 "2.)"             Pretty.<+> ( Pretty.align-                           ( "None of them are provided"-                               Pretty.<$$> "The JSON object is read from STDIN."+                           ( Pretty.vcat+                               [ "None of them are provided",+                                 "The JSON object is read from STDIN."+                               ]                            )                        ),           Pretty.indent 2 "3.)"             Pretty.<+> ( Pretty.align-                           ( "One of them is provided"-                               Pretty.<$$> "The JSON object is read from the supplied option."+                           ( Pretty.vcat+                               [ "One of them is provided",+                                 "The JSON object is read from the supplied option."+                               ]                            )                        )         ]@@ -117,7 +130,7 @@         EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure         EDE.Success output -> Text.Lazy.IO.putStr output -stdinParser :: Parsec.Parser Aeson.Object+stdinParser :: Parsec.Parser (HashMap Text Aeson.Value) stdinParser = Aeson.json >>= requireObject  readValue :: Options.ReadM Aeson.Value@@ -129,10 +142,10 @@     . Text.Encoding.encodeUtf8     . Text.pack -readObject :: Options.ReadM Aeson.Object+readObject :: Options.ReadM (HashMap Text Aeson.Value) readObject = readValue >>= requireObject -requireObject :: Fail.MonadFail m => Aeson.Value -> m Aeson.Object+requireObject :: Fail.MonadFail m => Aeson.Value -> m (HashMap Text Aeson.Value) requireObject = \case-  Aeson.Object obj -> pure obj+  Aeson.Object obj -> pure (Compat.toHashMapText obj)   _ -> fail "JSON value must be an object"
ede.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               ede-version:            0.3.2.0+version:            0.3.4.0 synopsis:   Templating language with similar syntax and features to Liquid or Jinja2. @@ -9,7 +9,7 @@ license-file:       LICENSE author:             Brendan Hay maintainer:         Brendan Hay <brendan.g.hay@gmail.com>-copyright:          Copyright (c) 2013-2020 Brendan Hay+copyright:          Copyright (c) 2013-2022 Brendan Hay stability:          Experimental category:           Text, Template, Web build-type:         Simple@@ -45,13 +45,16 @@   type:     git   location: git://github.com/brendanhay/ede.git +flag AttoparsecAeson+  description: Build attoparsec-aeson.+ common base   default-language: Haskell2010   ghc-options:     -Wall -funbox-strict-fields -fwarn-incomplete-uni-patterns-    -fwarn-incomplete-record-updates+    -fwarn-incomplete-record-updates -fwarn-unused-packages -  build-depends:    base >=4.12 && <5+  build-depends:    base >=4.13 && <5   autogen-modules:  Paths_ede   other-modules:    Paths_ede @@ -62,6 +65,7 @@     Text.EDE     Text.EDE.Filters     Text.EDE.Internal.AST+    Text.EDE.Internal.Compat     Text.EDE.Internal.Eval     Text.EDE.Internal.Filters     Text.EDE.Internal.Parser@@ -70,8 +74,8 @@     Text.EDE.Internal.Types    build-depends:-    , aeson                        >=0.7-    , bytestring                   >=0.9+    , aeson                        >=1.4+    , bytestring                   >=0.10.4     , comonad                      >=4.2     , directory                    >=1.2     , filepath                     >=1.2@@ -79,7 +83,7 @@     , lens                         >=4.0     , mtl                          >=2.1.3.1     , parsers                      >=0.12.1.1-    , prettyprinter                >=1.6+    , prettyprinter                >=1.7     , prettyprinter-ansi-terminal  >=1.1     , scientific                   >=0.3.1     , text                         >=1.2@@ -94,14 +98,22 @@   main-is:        Main.hs   ghc-options:    -threaded -rtsopts -with-rtsopts=-A128m   build-depends:-    , aeson                        >=0.8-    , attoparsec+    , attoparsec                   >=0.13     , bytestring                   >=0.10.4     , ede-    , optparse-applicative         >=0.11-    , prettyprinter                >=1.6+    , optparse-applicative         >=0.18+    , prettyprinter                >=1.7     , prettyprinter-ansi-terminal  >=1.1     , text                         >=1.2+    , unordered-containers         >=0.2.3++  if flag(AttoparsecAeson)+    build-depends:+      , aeson                      >=2.2+      , attoparsec-aeson           >=2.2+  else+    build-depends:+      , aeson                      >=1.4  test-suite tests   import:         base
lib/Text/EDE.hs view
@@ -3,7 +3,7 @@  -- | -- Module      : Text.EDE--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -109,12 +109,15 @@      -- ** Let Expressions     -- $let++    -- ** Set+    -- $set   ) where  import qualified Control.Monad as Monad import Data.Aeson ((.=))-import Data.Aeson.Types (Object)+import Data.Aeson.Types (Value) import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString import qualified Data.Foldable as Foldable@@ -125,9 +128,9 @@ import qualified Data.Text as Text import qualified Data.Text.Lazy as Text.Lazy import qualified Data.Text.Lazy.Builder as Text.Builder-import Data.Text.Prettyprint.Doc (Pretty (..)) import Data.Version (Version) import qualified Paths_ede as Paths+import Prettyprinter (Pretty (..)) import qualified System.Directory as Directory import qualified System.FilePath as FilePath import qualified Text.EDE.Internal.Eval as Eval@@ -211,14 +214,15 @@   -- | Strict 'ByteString' template definition.   ByteString ->   m (Result Template)-parseWith config f name =-  result failure resolve-    . Parser.runParser config name+parseWith config f name input =+  case Parser.runParser config name input of+    Success (u, is', es', bs) -> do+      is <- Foldable.foldrM include (Success (HashMap.singleton name u)) (HashMap.toList is')+      es <- Foldable.foldrM extends (Success mempty) (HashMap.toList es')+      pure (Template name u <$> is <*> es <*> pure bs)+    Failure err ->+      failure err   where-    resolve (u, is) =-      Foldable.foldrM include (Success (HashMap.singleton name u)) (HashMap.toList is)-        >>= result failure (success . Template name u)-     -- Presuming self is always in self's includes, see singleton above.     -- FIXME: utilise the list of deltas for failures     include (_, _) (Failure err) = failure err@@ -226,6 +230,11 @@       f config key delta         >>= result failure (success . mappend ss . _tmplIncl) +    extends (_, _) (Failure err) = failure err+    extends (key, delta :| _) (Success ss) =+      f config key delta+        >>= result failure (success . mappend ss . _tmplIncl)+ -- | 'HashMap' resolver for @include@ expressions. -- -- The 'identifier' component of the @include@ expression is treated as a lookup@@ -270,26 +279,30 @@     then failure ("file " <> pretty path <> " doesn't exist.")     else ByteString.readFile path >>= success --- | Render an 'Object' using the supplied 'Template'.+-- | Render an 'HashMap Text Value' using the supplied 'Template'. render ::   -- | Parsed 'Template' to render.   Template ->   -- | Bindings to make available in the environment.-  Object ->+  HashMap Text Value ->   Result Text.Lazy.Text render = renderWith mempty --- | Render an 'Object' using the supplied 'Template'.+-- | Render an 'HashMap Text Value' using the supplied 'Template'. renderWith ::   -- | Filters to make available in the environment.   HashMap Id Term ->   -- | Parsed 'Template' to render.   Template ->   -- | Bindings to make available in the environment.-  Object ->+  HashMap Text Value ->   Result Text.Lazy.Text-renderWith fs (Template _ u ts) =-  fmap Text.Builder.toLazyText . Eval.render ts fs u+renderWith fs t =+  fmap Text.Builder.toLazyText .+    Eval.render+      (_tmplIncl t <> _tmplExtends t)+      fs+      (_tmplExp t)  -- | /See:/ 'parse' eitherParse :: ByteString -> Either String Template@@ -312,7 +325,7 @@ -- | /See:/ 'render' eitherRender ::   Template ->-  Object ->+  HashMap Text Value ->   Either String Text.Lazy.Text eitherRender t = eitherResult . render t @@ -320,7 +333,7 @@ eitherRenderWith ::   HashMap Id Term ->   Template ->-  Object ->+  HashMap Text Value ->   Either String Text.Lazy.Text eitherRenderWith fs t = eitherResult . renderWith fs t @@ -333,15 +346,15 @@ -- -- >>> tmpl <- parse "{% if var %}\nHello, {{ var }}!\n{% else %}\nnegative!\n{% endif %}\n" :: Result Template ----- Then an 'Object' is defined containing the environment which will be+-- Then an 'HashMap Text Value' is defined containing the environment which will be -- available to the 'Template' during rendering: ----- >>> let env = fromPairs [ "var" .= "World" ] :: Object+-- >>> let env = fromPairs [ "var" .= "World" ] :: HashMap Text Value -- -- Note: the 'fromPairs' function above is a wrapper over Aeson's 'object'--- which removes the outer 'Object' 'Value' constructor, exposing the underlying 'HashMap'.+-- which removes the outer 'Data.Aeson.Object' 'Value' constructor, exposing the underlying 'HashMap'. ----- Then, the 'Template' is rendered using the 'Object' environment:+-- Then, the 'Template' is rendered using the 'HashMap Text Value' environment: -- -- >>> render tmpl env :: Result Text -- > Success "Hello, World!"@@ -379,7 +392,7 @@ -- which can be cached for future use. -- -- * Rendering takes a 'HashMap' of custom 'Fun's (functions available in the--- template context), an 'Object' as the binding environment, and a parsed+-- template context), an 'HashMap Text Value' as the binding environment, and a parsed -- 'Template' to subsitute the values into. -- The result is a Lazy 'Text.Lazy.Text' value containing the rendered output. @@ -487,12 +500,12 @@ -- -- Variables are substituted directly for their renderable representation. -- An error is raised if the varaible being substituted is not a literal type--- (ie. an 'Array' or 'Object') or doesn't exist in the supplied environment.+-- (ie. an 'Array' or 'HashMap Text Value') or doesn't exist in the supplied environment. -- -- > {{ var }} ----- Nested variable access is also supported for variables which resolve to an 'Object'.--- Dot delimiters are used to chain access through multiple nested 'Object's.+-- Nested variable access is also supported for variables which resolve to an 'HashMap Text Value'.+-- Dot delimiters are used to chain access through multiple nested 'HashMap Text Value's. -- The right-most accessor must resolve to a renderable type as with the previous -- non-nested variable access. --@@ -564,7 +577,7 @@  -- $loops ----- Iterating over an 'Array' or 'Object' can be acheived using the 'for ... in' section syntax.+-- Iterating over an 'Array' or 'HashMap Text Value' can be acheived using the 'for ... in' section syntax. -- Attempting to iterate over any other type will raise an error. -- -- Example:@@ -578,13 +591,13 @@ -- The iteration branch is rendering per item with the else branch being (which is optional) -- being rendered if the @{{ list }}@ variable is empty. ----- When iterating over an 'Object', a stable sort using key equivalence is applied, 'Array's+-- When iterating over an 'HashMap Text Value', a stable sort using key equivalence is applied, 'Array's -- are unmodified. -- -- The resulting binding within the iteration expression (in this case, @{{ var }}@) is--- an 'Object' containing the following keys:+-- an 'HashMap Text Value' containing the following keys: ----- * @key        :: Text@: They key if the loop target is an 'Object'+-- * @key        :: Text@: They key if the loop target is an 'HashMap Text Value' -- -- * @value      :: a@: The value of the loop target --@@ -620,8 +633,8 @@ -- Will render each item with its (1-based) loop index as a prefix, separated -- by a blank newline, without a trailing at the end of the document. ----- Valid loop targets are 'Object's, 'Array's, and 'String's, with only 'Object's--- having an available @{{ <var>.key }}@ in scope.+-- Valid loop targets are 'HashMap Text Value's, 'Array's, and 'String's, with+-- only 'HashMap Text Value's having an available @{{ <var>.key }}@ in scope.  -- $includes --@@ -696,3 +709,55 @@ -- > {{ var }} -- > ... -- > {% endlet %}++-- $set+--+-- You can also bind an identifier to whole templates which will be available within+-- the following expression scope. The identifier will be available in subsequent template.+--+-- For example:+--+-- > {% set var %}+-- >   ...+-- > {% endset %}+-- > ...+-- > {{ var }}++-- $block+--+-- Blocks are used for inheritance and act as both placeholders and replacements at the same time: The most powerful+-- part of @ED-E@ is template inheritance. Template inheritance allows you to build a base "skeleton" template that+-- contains all the common elements of your site and defines blocks that child templates can override.+--+-- Base template:+--+-- > <!doctype html>+-- > {% block head %}+-- > <title>{% block title %}{% endblock %}</title>+-- > {% endblock %}+-- > {% block body %}{% endblock %}+--+-- Child template:+--+-- > {% extends "base.html" %}+-- > {% block title %}Index{% endblock %}+-- > {% block head %}+-- >  {{ super }}+-- >  <style type="text/css">+-- >   .important { color: #336699; }+-- > </style>+-- > {% endblock %}+-- > {% block body %}+-- >  <h1>Index</h1>+-- >  <p class="important">+-- >    Welcome to my awesome homepage.+-- >  </p>+-- > {% endblock %}+--+-- The @{% extends %}@ tag is the key here. It tells the template engine that this template "extends" another template.+-- When the template system evaluates this template, it first locates the parent. The extends tag should be the first tag in the template.+--+-- As you can see it's also possible to render the contents of the parent block by calling super(). You can’t define multiple {% block %}+-- tags with the same name in the same template. This limitation exists because a block tag works in “both” directions. That is, a block+-- tag doesn’t just provide a placeholder to fill - it also defines the content that fills the placeholder in the parent. If there were two+-- similarly-named {% block %} tags in a template, that template's parent wouldn’t know which one of the blocks’ content to use.
lib/Text/EDE/Filters.hs view
@@ -1,6 +1,6 @@ -- | -- Module      : Text.EDE.Filters--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or
lib/Text/EDE/Internal/AST.hs view
@@ -3,7 +3,7 @@  -- | -- Module      : Text.EDE.Internal.AST--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -56,6 +56,21 @@ elet :: Maybe (Id, Exp a) -> Exp a -> Exp a elet m e = maybe e (\(i, b) -> Comonad.extract b :< ELet i b e) m {-# INLINEABLE elet #-}++eset :: Id -> Exp a -> Exp a -> Exp a+eset i e k =+  elet (Just (i, e)) k+{-# INLINEABLE eset #-}++eblock :: Id -> Exp a -> Exp a+eblock i e =+  Comonad.extract e :< EBlock i e+{-# INLINEABLE eblock #-}++eOverrideBlock :: Id -> Exp a -> Exp a -> Exp a+eOverrideBlock i b e =+  Comonad.extract b :< EOverrideBlock i b e+{-# INLINEABLE eOverrideBlock #-}  ecase ::   Exp a ->
+ lib/Text/EDE/Internal/Compat.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE CPP #-}++-- |+-- Module      : Text.EDE.Internal.Eval+-- Copyright   : (c) 2022 Brendan Hay <brendan.g.hay@gmail.com>+-- License     : This Source Code Form is subject to the terms of+--               the Mozilla Public License, v. 2.0.+--               A copy of the MPL can be found in the LICENSE file or+--               you can obtain it at http://mozilla.org/MPL/2.0/.+-- Maintainer  : Brendan Hay <brendan.g.hay@gmail.com>+-- Stability   : experimental+-- Portability : non-portable (GHC extensions)+--+-- /Warning/: this is an internal module, and does not have a stable+-- API or name. Functions in this module may not check or enforce+-- preconditions expected by public modules. Use at your own risk!+module Text.EDE.Internal.Compat+  ( toHashMapText,+    fromHashMapText,+  )+where++#if MIN_VERSION_aeson(2,0,0)+import Data.Aeson.KeyMap (toHashMapText, fromHashMapText)+#else+import Data.HashMap.Strict (HashMap)+import Data.Aeson.Types (Value)+import Data.Text (Text)++toHashMapText :: HashMap Text Value -> HashMap Text Value+toHashMapText = id++fromHashMapText :: HashMap Text Value -> HashMap Text Value+fromHashMapText = id+#endif
lib/Text/EDE/Internal/Eval.hs view
@@ -5,7 +5,7 @@  -- | -- Module      : Text.EDE.Internal.Eval--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -26,20 +26,22 @@ import Control.Monad.Trans (lift) import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson-import Data.Aeson.Types (Object, Value (..))+import Data.Aeson.Types (Value (..)) import qualified Data.Foldable as Foldable import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NonEmpty import Data.Scientific (isFloating)+import Data.Text (Text) import qualified Data.Text as Text import Data.Text.Lazy.Builder (Builder) import qualified Data.Text.Lazy.Builder as Text.Builder import Data.Text.Lazy.Builder.Scientific (FPFormat (Fixed), formatScientificBuilder) import Data.Text.Manipulate (toOrdinal)-import Data.Text.Prettyprint.Doc ((<+>))-import qualified Data.Text.Prettyprint.Doc as PP+import Prettyprinter ((<+>))+import qualified Prettyprinter as PP+import Text.EDE.Internal.Compat import Text.EDE.Internal.Filters (stdlib) import Text.EDE.Internal.Quoting import Text.EDE.Internal.Types@@ -47,9 +49,11 @@ import qualified Text.Trifecta.Delta as Trifecta.Delta  data Env = Env-  { _templates :: HashMap Id (Exp Delta),+  { _extends :: Bool,+    _templates :: HashMap Id (Exp Delta),     _quoted :: HashMap Id Term,-    _values :: HashMap Id Value+    _values :: HashMap Id Value,+    _blocks :: HashMap Id (Exp Delta)   }  type Context = ReaderT Env Result@@ -61,7 +65,7 @@   HashMap Id Value ->   Result Builder render ts fs e o =-  Reader.runReaderT (eval e >>= nf) (Env ts (stdlib <> fs) o)+  Reader.runReaderT (eval e >>= nf) (Env False ts (stdlib <> fs) o mempty)   where     nf (TVal v) = build (Trifecta.Delta.delta e) v     nf _ =@@ -83,6 +87,26 @@   x <- eval a   y <- eval b   binding d x y+eval (d :< EOverrideBlock i b e) =+  setExtended True $ do+    q <- HashMap.lookup i <$> Reader.asks _blocks+    case q of+      Nothing ->+        bindBlock (HashMap.insert i b) (eval e)+      Just eb ->+        bindBlock (HashMap.insert i (d :< ELet "super" b eb)) (eval e)+eval (_ :< EBlock i b) = do+  extends <- Reader.asks _extends+  q <- HashMap.lookup i <$> Reader.asks _blocks+  if extends then+    pure (qprim (String mempty))+  else do+    x <- eval b+    v <- lift (unquote i 0 x)+    maybe+      (pure x)+      (bind (HashMap.insert "super" v) . eval)+      q eval (_ :< ELet k rhs bdy) = do   q <- eval rhs   v <- lift (unquote k 0 q)@@ -162,22 +186,45 @@ eval (d :< EIncl i) = do   ts <- Reader.asks _templates   case HashMap.lookup i ts of-    Just e -> eval e+    Just e ->+      -- Don't inherit any blocks declared so far.+      setExtended False $+        bindBlock (\_ -> mempty) (eval e)     Nothing ->       throwError d $         "template"           <+> bold (pp i)           <+> "is not in scope:"           <+> PP.brackets (pp (Text.intercalate "," $ HashMap.keys ts))+eval (d :< EExt i) = do+  ts <- Reader.asks _templates+  case HashMap.lookup i ts of+    Just e ->+      setExtended False (eval e)+    Nothing ->+      throwError d $+        "template"+          <+> bold (pp i)+          <+> "is not in scope:"+          <+> PP.brackets (pp (Text.intercalate "," $ HashMap.keys ts)) {-# INLINEABLE eval #-} -bind :: (Object -> Object) -> Context a -> Context a+bind :: (HashMap Text Value -> HashMap Text Value) -> Context a -> Context a bind f = Reader.withReaderT (\x -> x {_values = f (_values x)}) {-# INLINEABLE bind #-} +bindBlock :: (HashMap Id (Exp Delta) -> HashMap Id (Exp Delta)) -> Context a -> Context a+bindBlock f = Reader.withReaderT (\x -> x {_blocks = f (_blocks x)})+{-# INLINEABLE bindBlock #-}++setExtended :: Bool -> Context a -> Context a+setExtended extends =+  Reader.withReaderT (\x -> x { _extends = extends })+{-# INLINEABLE setExtended #-}+ variable :: Delta -> Var -> Context Value variable d (Var is) =-  Reader.asks _values >>= go (NonEmpty.toList is) [] . Object+  Reader.asks _values >>= go (NonEmpty.toList is) [] . Object . fromHashMapText   where     go [] _ v = pure v     go (k : ks) r v = do@@ -189,8 +236,8 @@       where         cur = Var (k :| r) -        nest :: Value -> Context Object-        nest (Object o) = pure o+        nest :: Value -> Context (HashMap Text Value)+        nest (Object o) = pure (toHashMapText o)         nest x =           throwError d $             "variable"
lib/Text/EDE/Internal/Filters.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE ExtendedDefaultRules #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-}@@ -7,7 +8,7 @@  -- | -- Module      : Text.EDE.Internal.Filters--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -22,7 +23,7 @@ module Text.EDE.Internal.Filters where  import qualified Data.Aeson as Aeson-import Data.Aeson.Types (Array, Object, Value (..))+import Data.Aeson.Types (Value (..)) import qualified Data.Char as Char import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap@@ -33,9 +34,11 @@ import qualified Data.Text.Lazy as Text.Lazy import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding import qualified Data.Text.Manipulate as Text.Manipulate-import Data.Text.Prettyprint.Doc ((<+>)) import qualified Data.Text.Unsafe as Text.Unsafe+import Data.Vector (Vector) import qualified Data.Vector as Vector+import Prettyprinter ((<+>))+import Text.EDE.Internal.Compat import Text.EDE.Internal.Quoting import Text.EDE.Internal.Types @@ -110,8 +113,8 @@       qlist1 "init" initT initV,       "at" @: (\x i -> x Vector.! i :: Value),       -- object-      "keys" @: (HashMap.keys :: Object -> [Text]),-      "elems" @: (HashMap.elems :: Object -> [Value]),+      "keys" @: (HashMap.keys :: HashMap Text Value -> [Text]),+      "elems" @: (HashMap.elems :: HashMap Text Value -> [Value]),       -- , "map"        @: undefined       -- , "filter"     @: undefined       -- , "zip"        @: undefined@@ -119,7 +122,7 @@        -- polymorphic       "show" @: (Text.Lazy.Encoding.decodeUtf8 . Aeson.encode :: Value -> Text.Lazy.Text),-      "singleton" @: (pure :: Value -> Vector.Vector Value)+      "singleton" @: (pure :: Value -> Vector Value)       -- FIXME: existence checks currently hardcoded into the evaluator:       -- "default"       -- "defined"@@ -146,7 +149,7 @@   (Quote a, Quote b) =>   Id ->   (Text -> a) ->-  (Array -> b) ->+  (Vector Value -> b) ->   (Id, Term) qlist1 k f g = (k,) . TLam $ \case   TVal (String t) -> pure . quote k 0 $ f t@@ -161,12 +164,12 @@   (Quote a, Quote b, Quote c) =>   Id ->   (Text -> a) ->-  (Object -> b) ->-  (Array -> c) ->+  (HashMap Text Value -> b) ->+  (Vector Value -> c) ->   (Id, Term) qcol1 k f g h = (k,) . TLam $ \case   TVal (String t) -> pure . quote k 0 $ f t-  TVal (Object o) -> pure . quote k 0 $ g o+  TVal (Object o) -> pure . quote k 0 $ g (toHashMapText o)   TVal (Array v) -> pure . quote k 0 $ h v   x ->     Failure $@@ -178,7 +181,7 @@ tailT = text Text.Unsafe.unsafeTail initT = text Text.init -headV, lastV, tailV, initV :: Array -> Value+headV, lastV, tailV, initV :: Vector Value -> Value headV = vec Vector.unsafeHead lastV = vec Vector.unsafeLast tailV = vec (Array . Vector.unsafeTail)@@ -187,7 +190,7 @@ text :: (Text -> Text) -> Text -> Value text f = String . safe mempty Text.null f -vec :: (Array -> Value) -> Array -> Value+vec :: (Vector Value -> Value) -> Vector Value -> Value vec = safe (Array Vector.empty) Vector.null  safe :: b -> (a -> Bool) -> (a -> b) -> a -> b
lib/Text/EDE/Internal/Parser.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -12,7 +11,7 @@  -- | -- Module      : Text.EDE.Internal.Parser--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -31,12 +30,11 @@ import Control.Comonad.Cofree (Cofree ((:<))) import Control.Lens ((%=)) import qualified Control.Lens as Lens-import Control.Monad (MonadPlus, void, unless)+import Control.Monad (MonadPlus, unless, void) import Control.Monad.State.Strict (MonadState, StateT) import qualified Control.Monad.State.Strict as State import Control.Monad.Trans (lift)-import Data.Aeson.Types (Array, Object, Value (..))-import qualified Data.Bifunctor as Bifunctor+import Data.Aeson.Types (Value (..)) import Data.ByteString (ByteString) import qualified Data.Char as Char import Data.HashMap.Strict (HashMap)@@ -48,8 +46,10 @@ import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text.Encoding+import Data.Vector (Vector) import qualified Data.Vector as Vector import Text.EDE.Internal.AST+import Text.EDE.Internal.Compat import Text.EDE.Internal.Syntax import Text.EDE.Internal.Types import qualified Text.Parser.Expression as Expression@@ -63,7 +63,9 @@  data Env = Env   { _settings :: !Syntax,-    _includes :: HashMap Text (NonEmpty Delta)+    _includes :: HashMap Text (NonEmpty Delta),+    _extends :: HashMap Text (NonEmpty Delta),+    _blocks :: HashMap Text (Exp Delta)   }  $(Lens.makeLenses ''Env)@@ -73,9 +75,7 @@  type Parser m =   ( Monad m,-#if MIN_VERSION_base(4,13,0)     MonadFail m,-#endif     MonadState Env m,     Trifecta.TokenParsing m,     Trifecta.DeltaParsing m,@@ -89,9 +89,7 @@       Applicative,       Alternative,       Monad,-#if MIN_VERSION_base(4,13,0)       MonadFail,-#endif       MonadPlus,       Trifecta.Parsing,       Trifecta.CharParsing,@@ -123,24 +121,35 @@   Syntax ->   Text ->   ByteString ->-  Result (Exp Delta, HashMap Text (NonEmpty Delta))+  Result (Exp Delta, HashMap Text (NonEmpty Delta), HashMap Text (NonEmpty Delta), HashMap Text (Exp Delta)) runParser o n = res . Trifecta.parseByteString (runEDE run) pos   where-    run = State.runStateT (pragma *> document <* Trifecta.eof) (Env o mempty)+    run = State.runStateT (pragma *> document <* Trifecta.eof) (Env o mempty mempty mempty)      pos = Trifecta.Delta.Directed (Text.Encoding.encodeUtf8 n) 0 0 0 0      res = \case-      Trifecta.Success x -> Success (Bifunctor.second _includes x)+      Trifecta.Success (x, env) ->+        if null (_extends env) then+          Success (x, _includes env, _extends env, _blocks env)+        else+          -- In case the template extends another template we arrange the contained+          -- blocks in a way that make it easy to evaluate the "inheritance" mechanism.+          let+            x' =+              HashMap.foldrWithKey eOverrideBlock x (_blocks env)+          in+            Success (x', _includes env, _extends env, _blocks env)       Trifecta.Failure e -> Failure (Trifecta._errDoc e)  pragma :: Parser m => m () pragma =   void . Trifecta.many $ do-    !xs <- pragmal-      *> Trifecta.symbol "EDE_SYNTAX"-      *> Trifecta.sepBy field spaces-      <* trimr pragmar+    !xs <-+      pragmal+        *> Trifecta.symbol "EDE_SYNTAX"+        *> Trifecta.sepBy field spaces+        <* trimr pragmar      mapM_ (uncurry Lens.assign) xs   where@@ -186,6 +195,9 @@       cases,       loop,       include,+      set,+      block',+      extends',       binding,       raw,       comment@@ -194,7 +206,7 @@ block :: Parser m => String -> m a -> m a block k p =   Trifecta.try (multiLine (keyword k) p)-  <|> singleLine (keyword k) p+    <|> singleLine (keyword k) p  multiLine :: Parser m => m b -> m a -> m a multiLine s =@@ -259,6 +271,13 @@         (,) <$> (keyword "with" *> identifier)           <*> (Trifecta.symbol "=" *> term) +extends' :: Parser m => m (Exp Delta)+extends' = block "extends" $ do+  d <- Trifecta.position+  k <- Trifecta.stringLiteral+  extends %= HashMap.insertWith (<>) k (d :| [])+  pure (d :< EExt k)+ binding :: Parser m => m (Exp Delta) binding =   elet . Just@@ -270,6 +289,22 @@     <*> document     <* exit "endlet" +set :: Parser m => m (Exp Delta)+set =+  eset+  <$> block "set" identifier+  <*> document+  <* exit "endset"+  <*> document++block' :: Parser m => m (Exp Delta)+block' = do+  k <- block "block" identifier+  e <- document+  _ <- exit "endblock"+  blocks %= HashMap.insert k e+  pure (eblock k e)+ raw :: Parser m => m (Exp Delta) raw = ann (ELit <$> body)   where@@ -342,7 +377,7 @@ collection = ann (EVar <$> variable <|> ELit <$> col)   where     col =-      Object <$> object+      (Object . fromHashMapText) <$> object         <|> Array <$> array         <|> String <$> Trifecta.stringLiteral @@ -351,7 +386,7 @@   Bool <$> bool     <|> Number <$> number     <|> String <$> Trifecta.stringLiteral-    <|> Object <$> object+    <|> (Object . fromHashMapText) <$> object     <|> Array <$> array  number :: Parser m => m Scientific@@ -364,7 +399,7 @@   Trifecta.symbol "true" *> pure True     <|> Trifecta.symbol "false" *> pure False -object :: Parser m => m Object+object :: Parser m => m (HashMap Text Value) object = HashMap.fromList <$> Trifecta.braces (Trifecta.commaSep pair)   where     pair =@@ -372,7 +407,7 @@         <$> (Trifecta.stringLiteral <* Trifecta.spaces)         <*> (Trifecta.char ':' *> Trifecta.spaces *> literal) -array :: Parser m => m Array+array :: Parser m => m (Vector Value) array = Vector.fromList <$> Trifecta.brackets (Trifecta.commaSep literal)  operator :: Parser m => Text -> m Delta@@ -413,10 +448,10 @@  triml :: Parser m => m a -> m a triml p = do-    c <- Trifecta.Delta.column <$> Trifecta.position-    if c == 0-        then Trifecta.spaces *> p-        else fail "left whitespace removal failed"+  c <- Trifecta.Delta.column <$> Trifecta.position+  if c == 0+    then Trifecta.spaces *> p+    else fail "left whitespace removal failed"  trimr :: Parser m => m a -> m a trimr p = p <* Trifecta.newline -- <* Trifecta.newline
lib/Text/EDE/Internal/Quoting.hs view
@@ -1,5 +1,5 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE ExtendedDefaultRules #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}@@ -8,7 +8,7 @@  -- | -- Module      : Text.EDE.Internal.Quoting--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -26,9 +26,10 @@ import Control.Monad ((>=>)) import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as Aeson-import Data.Aeson.Types (Array, Object, Value (..))+import Data.Aeson.Types (Value (..)) import qualified Data.Bifunctor as Bifunctor import qualified Data.ByteString.Char8 as ByteString.Char8+import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.List (sortBy) import Data.Ord (comparing)@@ -41,15 +42,18 @@ import Data.Text.Lazy.Builder (Builder) import qualified Data.Text.Lazy.Builder as Text.Builder import Data.Text.Manipulate (toOrdinal)-import Data.Text.Prettyprint.Doc (Pretty (..), (<+>))-import qualified Data.Text.Prettyprint.Doc as PP+import Data.Vector (Vector) import qualified Data.Vector as Vector+import Prettyprinter (Pretty (..), (<+>))+import qualified Prettyprinter as PP import Text.EDE.Internal.Types import Text.Trifecta.Delta (Delta) import qualified Text.Trifecta.Delta as Trifecta.Delta import qualified Text.Trifecta.Rendering as Trifecta.Rendering -default (AnsiDoc, Double, Integer)+#if MIN_VERSION_aeson(2,0,0)+import Data.Aeson.KeyMap (KeyMap)+#endif  -- | A HOAS representation of (possibly partially applied) values -- in the environment.@@ -96,6 +100,14 @@  instance Unquote Value +#if MIN_VERSION_aeson(2,0,0)+instance Unquote (KeyMap Value)+#endif++instance Unquote (HashMap Text Value)++instance Unquote (Vector Value)+ instance Unquote Text  instance Unquote [Text]@@ -108,10 +120,6 @@  instance Unquote Scientific -instance Unquote Object--instance Unquote Array- instance Unquote Int where   unquote k n =     unquote k n@@ -164,6 +172,14 @@  instance Quote Value +#if MIN_VERSION_aeson(2,0,0)+instance Quote (KeyMap Value)+#endif++instance Quote (HashMap Text Value)++instance Quote (Vector Value)+ instance Quote [Value]  instance Quote Text@@ -181,10 +197,6 @@ instance Quote Double  instance Quote Scientific--instance Quote Object--instance Quote Array  instance Quote Builder where   quote k n = quote k n . Text.Builder.toLazyText
lib/Text/EDE/Internal/Syntax.hs view
@@ -2,7 +2,7 @@  -- | -- Module      : Text.EDE.Internal.Syntax--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -104,7 +104,11 @@       "_",       ".",       "true",-      "false"+      "false",+      "set",+      "endset",+      "block",+      "endblock"     ]  pragmaStyle :: TokenParsing m => IdentifierStyle m
lib/Text/EDE/Internal/Types.hs view
@@ -11,7 +11,7 @@  -- | -- Module      : Text.EDE.Internal.Types--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or@@ -30,7 +30,7 @@ import Control.Comonad.Cofree (Cofree) import qualified Control.Lens as Lens import qualified Data.Aeson as Aeson-import Data.Aeson.Types (Object, Pair, Value (..))+import Data.Aeson.Types (Pair, Value (..)) import qualified Data.Functor.Classes as Functor.Classes import Data.HashMap.Strict (HashMap) import qualified Data.List as List@@ -38,9 +38,10 @@ import qualified Data.List.NonEmpty as NonEmpty import Data.Text (Text) import qualified Data.Text as Text-import Data.Text.Prettyprint.Doc (Doc, Pretty (..))-import qualified Data.Text.Prettyprint.Doc as PP-import qualified Data.Text.Prettyprint.Doc.Render.Terminal as PP+import Prettyprinter (Doc, Pretty (..))+import qualified Prettyprinter as PP+import qualified Prettyprinter.Render.Terminal as PP+import Text.EDE.Internal.Compat import Text.Trifecta.Delta (Delta, HasDelta) import qualified Text.Trifecta.Delta as Trifecta.Delta @@ -86,7 +87,7 @@ $(Lens.makePrisms ''Result)  instance Monad Result where-  return = Success+  return = pure   {-# INLINE return #-}    Success x >>= k = k x@@ -94,7 +95,7 @@   {-# INLINE (>>=) #-}  instance Applicative Result where-  pure = return+  pure = Success   {-# INLINE pure #-}    Success f <*> Success x = Success (f x)@@ -165,7 +166,9 @@ data Template = Template   { _tmplName :: !Text,     _tmplExp :: !(Exp Delta),-    _tmplIncl :: HashMap Id (Exp Delta)+    _tmplIncl :: HashMap Id (Exp Delta),+    _tmplExtends :: HashMap Id (Exp Delta),+    _tmplBlocks :: HashMap Id (Exp Delta)   }   deriving (Eq) @@ -202,9 +205,12 @@   | EFun !Id   | EApp !a !a   | ELet !Id !a !a+  | EBlock !Id !a+  | EOverrideBlock !Id !a !a   | ECase !a [Alt a]   | ELoop !Id !a !a   | EIncl !Text+  | EExt !Text   deriving (Eq, Show, Functor)  instance Functor.Classes.Eq1 ExpF where@@ -213,11 +219,14 @@   liftEq _ (EFun a) (EFun b) = a == b   liftEq c (EApp a1 a2) (EApp b1 b2) = a1 `c` b1 && a2 `c` b2   liftEq c (ELet a0 a1 a2) (ELet b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2+  liftEq c (EBlock a0 a1) (EBlock b0 b1) = a0 == b0 && a1 `c` b1+  liftEq c (EOverrideBlock a0 a1 a2) (EOverrideBlock b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2   liftEq c (ECase a as) (ECase b bs) = a `c` b && (List.all (uncurry altEq) $ zip as bs)     where       altEq (pA, a') (pB, b') = pA == pB && a' `c` b'   liftEq c (ELoop a0 a1 a2) (ELoop b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2   liftEq _ (EIncl a) (EIncl b) = a == b+  liftEq _ (EExt a) (EExt b) = a == b   liftEq _ _ _ = False  type Exp = Cofree ExpF@@ -228,15 +237,15 @@ -- | Unwrap a 'Value' to an 'Object' safely. -- -- See Aeson\'s documentation for more details.-fromValue :: Value -> Maybe Object-fromValue (Object o) = Just o+fromValue :: Value -> Maybe (HashMap Text Value)+fromValue (Object o) = Just (toHashMapText o) fromValue _ = Nothing  -- | Create an 'Object' from a list of name/value 'Pair's. -- -- See Aeson\'s documentation for more details.-fromPairs :: [Pair] -> Object+fromPairs :: [Pair] -> HashMap Text Value fromPairs xs =   case Aeson.object xs of-    Object o -> o+    Object o -> toHashMapText o     _other -> mempty
test/Main.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-}  -- Module      : Main--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com> -- License     : This Source Code Form is subject to the terms of --               the Mozilla Public License, v. 2.0. --               A copy of the MPL can be found in the LICENSE file or
+ test/resources/ext-base.ede view
@@ -0,0 +1,7 @@+{}+---+<!doctype html>+{% block head %}+<title>{% block title %}{% endblock %}</title>+{% endblock %}+{% block body %}{% endblock %}
+ test/resources/ext-base.golden view
@@ -0,0 +1,2 @@+<!doctype html>+<title></title>
+ test/resources/ext-child.ede view
@@ -0,0 +1,16 @@+{}+---+{% extends "ext-base.ede" %}+{% block title %}Index{% endblock %}+{% block head %}+  {{ super }}+  <style type="text/css">+    .important { color: #336699; }+  </style>+{% endblock %}+{% block body %}+  <h1>Index</h1>+  <p class="important">+    Welcome to my awesome homepage.+  </p>+{% endblock %}
+ test/resources/ext-child.golden view
@@ -0,0 +1,13 @@+{}+---+<!doctype html>+  <title>Index</title>++  <style type="text/css">+    .important { color: #336699; }+  </style>+  <h1>Index</h1>+  <p class="important">+    Welcome to my awesome homepage.+  </p>+
test/resources/polymorphic-show.golden view
@@ -4,4 +4,4 @@ 1.0 ["a","b","c","d","e"] [1,2,3,4,5]-{"foo":"x","bar":"y"}+{"bar":"y","foo":"x"}
+ test/resources/set.ede view
@@ -0,0 +1,17 @@+{+  "var": true+}+---+{% set x %}++  {% set y %}+    This is a nested block {{var}}+  {% endset %}++  This is a big expression! {{var}}++  {{ y }}++{% endset %}++{{x}}
+ test/resources/set.golden view
@@ -0,0 +1,8 @@++++  This is a big expression! true++      This is a nested block true++
test/resources/whitespace.golden view
@@ -6,6 +6,8 @@     +               test  +