packages feed

doctemplates 0.8.3 → 0.11.1

raw patch · 8 files changed

Files

README.md view
@@ -107,6 +107,8 @@   rendered verbatim.  (Note that no escaping is done;   the assumption is that the calling program will escape   the strings appropriately for the output format.)+- If the value of the variable is a boolean value, it+  will be rendered as `true` if true, or as `false` if false. - If the value is a list, the values will be concatenated. - If the value is a map, the string `true` will be rendered. - Every other value will be rendered as the empty string.@@ -116,8 +118,6 @@  - If the value is a number, it will be rendered as an   integer if possible, otherwise as a floating-point number.-- If the value is a JSON boolean, it will be rendered as `true`-  if true, and as the empty string if false.   ## Conditionals@@ -126,10 +126,17 @@ matched delimiters) and ends with `endif` (enclosed in matched delimiters).  It may optionally contain an `else` (enclosed in matched delimiters).  The `if` section is used if-`variable` has a non-empty value, otherwise the `else`-section is used (if present).  (Note that even the-string `false` counts as a true value.) Examples:+`variable` has a true value, otherwise the `else`+section is used (if present).  The following values+count as true: +- any map+- any array containing at least one true value+- any nonempty string (even `false`)+- boolean True++Examples:+ ``` $if(foo)$bar$endif$ @@ -187,7 +194,7 @@  A for loop begins with `for(variable)` (enclosed in matched delimiters) and ends with `endfor` (enclosed in matched-delimiters.+delimiters).  - If `variable` is an array, the material inside the loop will   be evaluated repeatedly, with `variable` being set to each
changelog.md view
@@ -1,5 +1,64 @@ # doctemplates +## 0.11.1++  * Add `getVariables`, returning free variables of a template [API change].+    Returns variable names in order of first occurrence, without+    duplicates, excluding variables bound inside for loops (the loop+    variable, fields underneath it, and the anaphoric variable `it`).++  * Fix off-by-one error in `alpha` pipe.++  * Fix `roman` pipe for numbers >= 4000.++  * Fix `reverse`, `alpha`, `roman` pipes on multi-chunk Docs.++## 0.11.0.1++  * Bump version bounds for doclayout.++  * Fix typos.++## 0.11++  * Remove HsYAML depenedency.++  * Remove upper bound for criterion.++## 0.10.0.2++  * Use doclayout 0.4.++## 0.10.0.1++  * Don't rely on aeson Object being implemented as a HashMap.+    This change is needed for doctemplates to compile against aeson 2.0.0.0.++## 0.10++  * Change rendering and conditional behavior with booleans.+    Previously, `$if(foo)$` evaluated to false iff `foo`+    would render as the empty string. This forced us to render+    a boolean False value as an empty string, rather than `false`.+    And this has caused various problems with templates+    (#16, jgm/pandoc#7402).  Now, boolean False values render as+    `false` -- just as True values render as `true`.  And conditionals+    are now sensitive to booleans, so `$if(foo)$` evaluates to false+    when `foo` is a boolean False value, even though it would render+    as the nonempty string `false`.++## 0.9++  * Add BoolVal constructor to Val.  This gives a smoother+    interface with JSON and YAML.  [API change]++  * Remove overlapping instances by generalizing+    `ToContext String String` and `FromContext String String`+    to `TemplateTarget [a] => ToContext [a] [a]` and+    `TemplateTarget [a] => FromContext [a] [a]`.+    Remove the instance `ToContext String (Doc String)`.+    Remove redundant constraints.  (#9, favonia) [API change]+ ## 0.8.3    * Properly handle nested loops (#15).  Previously "it" was
doctemplates.cabal view
@@ -1,5 +1,5 @@ name:                doctemplates-version:             0.8.3+version:             0.11.1 synopsis:            Pandoc-style document templates description:         This is the text templating system used by pandoc.                      It supports variable interpolation, iteration,@@ -15,13 +15,12 @@ copyright:           2016-19 John MacFarlane category:            Text build-type:          Simple--- extra-source-files:-data-files:          README.md+extra-doc-files:     README.md                      changelog.md extra-source-files:  test/*.test                      test/*.txt                      test/*.tex-cabal-version:       >=1.10+cabal-version:       2.0  library   hs-source-dirs:      src@@ -32,15 +31,13 @@                        safe,                        text-conversions,                        aeson,-                       HsYAML >= 0.2 && < 0.3,                        text,-                       doclayout >= 0.3 && < 0.4,-                       containers,+                       doclayout >= 0.4 && < 0.7,+                       containers >= 0.6.0.1,                        vector,                        filepath,                        parsec,                        mtl,-                       unordered-containers,                        scientific   if !impl(ghc >= 8.0)     build-depends:     semigroups == 0.18.*@@ -53,7 +50,7 @@   main-is:             test.hs   build-depends:       base,                        doctemplates,-                       doclayout >= 0.3 && < 0.4,+                       doclayout >= 0.4 && < 0.7,                        containers,                        aeson,                        Glob,@@ -72,9 +69,9 @@   Main-Is:         bench.hs   Hs-Source-Dirs:  bench   Build-Depends:   doctemplates,-                   doclayout >= 0.3 && < 0.4,+                   doclayout >= 0.4 && < 0.7,                    base >= 4.8 && < 5,-                   criterion >= 1.0 && < 1.6,+                   criterion >= 1.0,                    filepath,                    aeson,                    text,
src/Text/DocTemplates.hs view
@@ -104,6 +104,8 @@     verbatim. (Note that no escaping is done; the assumption is that the     calling program will escape the strings appropriately for the output     format.)+-   If the value of the variable is a boolean value, it will be rendered+    as @true@ if true, or as @false@ if false. -   If the value is a list, the values will be concatenated. -   If the value is a map, the string @true@ will be rendered. -   Every other value will be rendered as the empty string.@@ -113,18 +115,22 @@  -   If the value is a number, it will be rendered as an integer if     possible, otherwise as a floating-point number.--   If the value is a JSON boolean, it will be rendered as @true@ if-    true, and as the empty string if false.  == Conditionals  A conditional begins with @if(variable)@ (enclosed in matched delimiters) and ends with @endif@ (enclosed in matched delimiters). It may optionally contain an @else@ (enclosed in matched delimiters). The-@if@ section is used if @variable@ has a non-empty value, otherwise the-@else@ section is used (if present). (Note that even the string @false@-counts as a true value.) Examples:+@if@ section is used if @variable@ has a true value, otherwise the+@else@ section is used (if present). The following values count as true: +-   any map+-   any array containing at least one true value+-   any nonempty string (even @false@)+-   boolean True++Examples:+ > $if(foo)$bar$endif$ > > $if(foo)$@@ -399,6 +405,7 @@ -}  module Text.DocTemplates ( renderTemplate+                         , getVariables                          , compileTemplate                          , compileTemplateFile                          , applyTemplate@@ -418,7 +425,7 @@ import Text.DocTemplates.Parser (compileTemplate) import Text.DocTemplates.Internal ( TemplateMonad(..), Context(..),             Val(..), ToContext(..), FromContext(..), TemplateTarget,-            Template, renderTemplate )+            Template, renderTemplate, getVariables )  -- | Compile a template from a file.  IO errors will be -- raised as exceptions; template parsing errors result in
src/Text/DocTemplates/Internal.hs view
@@ -22,6 +22,7 @@  module Text.DocTemplates.Internal       ( renderTemplate+      , getVariables       , TemplateMonad(..)       , Context(..)       , Val(..)@@ -37,11 +38,9 @@  import Data.Text.Conversions (FromText(..), ToText(..)) import Data.Aeson (Value(..), ToJSON(..), FromJSON(..), Result(..), fromJSON)-import Data.YAML (ToYAML(..), FromYAML(..), Node(..), Scalar(..)) import Control.Monad.Identity import qualified Control.Monad.State.Strict as S import Data.Char (chr, ord)-import Data.Maybe (fromMaybe) import qualified Data.Text.Read as T import qualified Data.Text as T import qualified Data.Text.IO as TIO@@ -53,10 +52,10 @@ import GHC.Generics (Generic) import Data.Text (Text) import qualified Data.Map as M-import qualified Data.HashMap.Strict as H+import Data.Containers.ListUtils (nubOrd) import qualified Data.Vector as V import Data.Scientific (floatingOrInteger)-import Data.List (intersperse)+import Data.List (intersperse, isPrefixOf) #if MIN_VERSION_base(4,11,0) #else import Data.Semigroup@@ -129,7 +128,7 @@   mappend = (<>)  type TemplateTarget a =-  (Monoid a, IsString a, HasChars a, ToText a, FromText a)+  (HasChars a, ToText a, FromText a)  -- | A 'Context' defines values for template's variables. newtype Context a = Context { unContext :: M.Map Text (Val a) }@@ -141,6 +140,7 @@     SimpleVal  (Doc a)   | ListVal    [Val a]   | MapVal     (Context a)+  | BoolVal    Bool   | NullVal   deriving (Show, Traversable, Foldable, Functor, Data, Typeable) @@ -166,25 +166,19 @@ instance ToContext a a => ToContext a (Doc a) where   toVal     = SimpleVal --- This is needed because otherwise the compiler tries to--- match on ToContext a [b], with a = b = Char, even though--- we don't have ToContext Char Char.  I don't understand why.-instance {-# OVERLAPPING #-} ToContext String String where-  toVal    = SimpleVal . DL.literal--instance {-# OVERLAPPING #-} ToContext String (Doc String) where-  toVal    = SimpleVal- instance ToContext a b => ToContext a [b] where   toVal     = ListVal . map toVal +instance {-# OVERLAPPING #-} TemplateTarget [a] => ToContext [a] [a] where+  toVal    = SimpleVal . DL.literal+ instance ToContext a b => ToContext a (M.Map Text b) where   toVal     = MapVal . toContext   toContext = Context . M.map toVal  instance TemplateTarget a => ToContext a Bool where-  toVal True  = SimpleVal "true"-  toVal False = NullVal+  toVal True  = BoolVal True+  toVal False = BoolVal False  instance TemplateTarget a => ToContext a Value where   toContext x = case fromJSON x of@@ -212,10 +206,7 @@   fromVal (SimpleVal x) = Just (DL.render Nothing x)   fromVal _             = Nothing --- This is needed because otherwise the compiler tries to--- match on FromContext a [b], with a = b = Char, even though--- we don't have FromContext Char Char.  I don't understand why.-instance {-# OVERLAPPING #-} FromContext String String where+instance {-# OVERLAPPING #-} TemplateTarget [a] => FromContext [a] [a] where   fromVal (SimpleVal x) = Just (DL.render Nothing x)   fromVal _             = Nothing @@ -232,9 +223,8 @@                               case floatingOrInteger n of                                   Left (r :: Double)   -> show r                                   Right (i :: Integer) -> show i-      Bool True   -> return $ SimpleVal "true"-      Object o    -> MapVal . Context . M.fromList . H.toList <$>-                       mapM parseJSON o+      Bool b      -> return $ BoolVal b+      Object _    -> MapVal . Context <$> parseJSON v       _           -> return NullVal  instance TemplateTarget a => FromJSON (Context a) where@@ -244,28 +234,6 @@       MapVal o -> return o       _        -> fail "Expecting MapVal" -instance TemplateTarget a => FromYAML (Val a) where-  parseYAML v =-    case v of-      Mapping _ _ m -> MapVal . Context . M.fromList <$>-                           mapM (\(key, val) -> do-                                  val' <- parseYAML val-                                  key' <- parseYAML key-                                  return (key', val')) (M.toList m)-      Sequence _ _ xs -> ListVal <$> mapM parseYAML xs-      Scalar _ (SStr t) -> return $ SimpleVal $ fromString . fromText $ t-      Scalar _ (SFloat n) -> return $ SimpleVal $ fromString . show $ n-      Scalar _ (SInt n) -> return $ SimpleVal $ fromString . show $ n-      Scalar _ (SBool True) -> return $ SimpleVal "true"-      _           -> return NullVal--instance TemplateTarget a => FromYAML (Context a) where-  parseYAML v = do-    val <- parseYAML v-    case val of-      MapVal o -> return o-      _        -> fail "Expecting MapVal"- instance TemplateTarget a => ToJSON (Context a) where   toJSON (Context m) = toJSON m @@ -274,15 +242,7 @@   toJSON (MapVal m) = toJSON m   toJSON (ListVal xs) = toJSON xs   toJSON (SimpleVal d) = toJSON $ toText $ DL.render Nothing d--instance TemplateTarget a => ToYAML (Context a) where-  toYAML (Context m) = toYAML m--instance TemplateTarget a => ToYAML (Val a) where-  toYAML NullVal = toYAML (Nothing :: Maybe Text)-  toYAML (MapVal m) = toYAML m-  toYAML (ListVal xs) = toYAML xs-  toYAML (SimpleVal d) = toYAML $ toText $ DL.render Nothing d+  toJSON (BoolVal b) = toJSON b  mapDoc :: TemplateTarget a => (Doc a -> Doc a) -> Val a -> Val a mapDoc f val =@@ -290,8 +250,14 @@     SimpleVal d        -> SimpleVal (f d)     MapVal (Context m) -> MapVal (Context $ M.map (mapDoc f) m)     ListVal xs         -> ListVal $ map (mapDoc f) xs+    BoolVal b          -> BoolVal b     NullVal            -> NullVal +-- | The full rendered text of a 'Doc', regardless of how it is+-- divided into chunks.+renderedText :: TemplateTarget a => Doc a -> Text+renderedText = toText . DL.render Nothing+ mapText :: TemplateTarget a => (Text -> Text) -> Val a -> Val a mapText f val =   runIdentity (traverse (return . fromText . f . toText) val)@@ -303,6 +269,7 @@            SimpleVal d        -> T.length . toText $ DL.render Nothing d            MapVal (Context m) -> M.size m            ListVal xs         -> length xs+           BoolVal _          -> 0            NullVal            -> 0 applyPipe ToUppercase val = mapText T.toUpper val applyPipe ToLowercase val = mapText T.toLower val@@ -336,19 +303,22 @@ applyPipe Reverse val =   case val of     ListVal xs  -> ListVal (reverse xs)-    SimpleVal{} -> mapText T.reverse val+    SimpleVal d -> SimpleVal $ DL.literal . fromText . T.reverse $+                     renderedText d     _           -> val applyPipe Chomp val = mapDoc DL.chomp val-applyPipe ToAlpha val = mapText toAlpha val-  where toAlpha t =-          case T.decimal t of-            Right (y,"") -> fromString [chr (ord 'a' + (y `mod` 26) - 1)]-            _            -> t-applyPipe ToRoman val = mapText toRoman' val-  where toRoman' t =-         case T.decimal t of-           Right (y,"") -> fromMaybe t (toRoman y)-           _            -> t+applyPipe ToAlpha val = mapDoc toAlpha val+  where toAlpha d =+          case T.decimal (renderedText d) of+            Right (y,"")+              | y > (0 :: Int) -> fromString+                                    [chr (ord 'a' + ((y - 1) `mod` 26))]+            _            -> d+applyPipe ToRoman val = mapDoc toRoman' val+  where toRoman' d =+         case T.decimal (renderedText d) of+           Right (y,"") -> maybe d (DL.literal . fromText) (toRoman y)+           _            -> d applyPipe NoWrap val = mapDoc DL.nowrap val applyPipe (Block align n border) val =   let constructor = case align of@@ -372,8 +342,8 @@ -- | Convert number 0 < x < 4000 to lowercase roman numeral. toRoman :: Int -> Maybe Text toRoman x-  | x >= 1000-  , x < 4000  = ("m" <>) <$> toRoman (x - 1000)+  | x >= 4000 = Nothing+  | x >= 1000 = ("m" <>) <$> toRoman (x - 1000)   | x >= 900  = ("cm" <>) <$> toRoman (x - 900)   | x >= 500  = ("d" <>) <$> toRoman (x - 500)   | x >= 400  = ("cd" <>) <$> toRoman (x - 400)@@ -400,18 +370,34 @@     Just v' -> multiLookup vs v' multiLookup _ _ = NullVal -resolveVariable :: TemplateTarget a => Variable -> Context a -> [Doc a]+-- The Bool indicates whether it's a true or false value.+data Resolved a = Resolved Bool [Doc a]+   deriving (Show, Read, Data, Typeable, Generic, Eq, Ord,+             Foldable, Traversable, Functor)++instance Semigroup (Resolved a) where+  Resolved b1 x1 <> Resolved b2 x2 = Resolved (b1 || b2) (x1 <> x2)++instance Monoid (Resolved a) where+  mappend = (<>)+  mempty = Resolved False []++resolveVariable :: TemplateTarget a+                => Variable -> Context a -> Resolved a resolveVariable v ctx = resolveVariable' v (MapVal ctx) -resolveVariable' :: TemplateTarget a => Variable -> Val a -> [Doc a]+resolveVariable' :: TemplateTarget a+                 => Variable -> Val a -> Resolved a resolveVariable' v val =   case applyPipes (varPipes v) $ multiLookup (varParts v) val of-    ListVal xs    -> concatMap (resolveVariable' mempty) xs+    ListVal xs    -> mconcat $ map (resolveVariable' mempty) xs     SimpleVal d-      | DL.isEmpty d -> []-      | otherwise    -> [removeFinalNl d]-    MapVal _      -> ["true"]-    NullVal       -> []+      | DL.isEmpty d -> Resolved False []+      | otherwise    -> Resolved True [removeFinalNl d]+    MapVal _      -> Resolved True ["true"]+    BoolVal True  -> Resolved True ["true"]+    BoolVal False -> Resolved False ["false"]+    NullVal       -> Resolved False []  removeFinalNl :: Doc a -> Doc a removeFinalNl DL.NewLine        = mempty@@ -448,6 +434,28 @@                => Template a -> b -> Doc a renderTemplate t x = S.evalState (renderTemp t (toContext x)) 0 +-- | Retrieve the names of all free variables used in a template,+-- in the order in which they first occur, without duplicates.+-- The parts of a multipart variable (@foo.bar@) are separated+-- by periods.  Variables bound inside a @for@ loop (the loop+-- variable, fields underneath it, and the anaphoric variable+-- @it@) are not included.+getVariables :: Template a -> [Text]+getVariables = nubOrd . map (T.intercalate ".") . go+ where+  go (Interpolate v)       = [varParts v]+  go (Conditional v t1 t2) = varParts v : go t1 <> go t2+  go (Iterate v t1 t2)     = varParts v :+                             filter (not . boundBy v) (go t1) <> go t2+  go (Nested t)            = go t+  go (Partial _ t)         = go t+  go (Literal _)           = []+  go (Concat t1 t2)        = go t1 <> go t2+  go Empty                 = []+  -- inside the body of @for(v)@, variables under v are rebound+  -- to the current list item, and "it" refers to the item itself+  boundBy v parts = varParts v `isPrefixOf` parts || take 1 parts == ["it"]+ updateColumn :: TemplateTarget a => Doc a -> RenderState (Doc a) updateColumn x = do   S.modify $ DL.updateColumn x@@ -456,12 +464,13 @@ renderTemp :: forall a . TemplateTarget a            => Template a -> Context a -> RenderState (Doc a) renderTemp (Literal t) _ = updateColumn t-renderTemp (Interpolate v) ctx = updateColumn $ mconcat $ resolveVariable v ctx+renderTemp (Interpolate v) ctx =+  case resolveVariable v ctx of+    Resolved _ xs -> updateColumn (mconcat xs) renderTemp (Conditional v ift elset) ctx =-  let res = resolveVariable v ctx-   in case res of-        [] -> renderTemp elset ctx-        _  -> renderTemp ift ctx+  case resolveVariable v ctx of+    Resolved False _ -> renderTemp elset ctx+    Resolved True _  -> renderTemp ift ctx renderTemp (Iterate v t sep) ctx = do   xs <- withVariable v ctx (renderTemp t)   sep' <- renderTemp sep ctx
+ test/boolean.test view
@@ -0,0 +1,13 @@+{ "foo": true,+  "bar": false+}+.+$foo$+$bar$+$if(foo)$XXX$else$YYY$endif$+$if(bar)$XXX$else$YYY$endif$+.+true+false+XXX+YYY
− test/keyval.txt
@@ -1,1 +0,0 @@-
test/test.hs view
@@ -80,6 +80,40 @@                        :: Val T.Text) mempty))                   Left e  -> T.pack e       res @?= "hello this is a test of the wrapping\nhello this\nis a test\nof the\nwrapping"+  , testCase "alpha pipe" $ do+      (templ :: Either String (Template T.Text)) <-+        compileTemplate "foo" "$one/alpha$ $twentysix/alpha$ $twentyseven/alpha$ $zero/alpha$"+      let res :: T.Text+          res = case templ of+                  Right t -> render Nothing+                   (renderTemplate t (object [ "one" .= (1 :: Int)+                                             , "twentysix" .= (26 :: Int)+                                             , "twentyseven" .= (27 :: Int)+                                             , "zero" .= (0 :: Int) ]))+                  Left e  -> T.pack e+      res @?= "a z a 0"+  , testCase "getVariables" $ do+      (res :: Either String (Template T.Text)) <-+        compileTemplate "" "$b$$a$$if(foo.bar)$$b$$else$$baz$$endif$"+      fmap getVariables res @?= Right ["b", "a", "foo.bar", "baz"]+  , testCase "getVariables excludes loop-bound variables" $ do+      (res :: Either String (Template T.Text)) <-+        compileTemplate ""+          "$for(employee)$$employee.name$: $it.salary$ $company$$sep$$separator$$endfor$"+      fmap getVariables res @?= Right ["employee", "company", "separator"]+  , testCase "getVariables excludes it in bracketed loop" $ do+      (res :: Either String (Template T.Text)) <-+        compileTemplate "" "$items[, ]$"+      fmap getVariables res @?= Right ["items"]+  , testCase "getVariables with nested loops" $ do+      (res :: Either String (Template T.Text)) <-+        compileTemplate ""+          "$for(a)$$for(a.b)$$it$$a.c$$x$$endfor$$endfor$"+      fmap getVariables res @?= Right ["a", "x"]+  , testCase "getVariables includes it outside of loops" $ do+      (res :: Either String (Template T.Text)) <-+        compileTemplate "" "$it$"+      fmap getVariables res @?= Right ["it"]   ]  {- The test "golden" files are structured as follows: