doctemplates 0.9 → 0.11.1
raw patch · 8 files changed
Files
- README.md +12/−5
- changelog.md +47/−0
- doctemplates.cabal +8/−11
- src/Text/DocTemplates.hs +13/−6
- src/Text/DocTemplates/Internal.hs +74/−63
- test/boolean.test +1/−1
- test/keyval.txt +0/−1
- test/test.hs +34/−0
README.md view
@@ -108,7 +108,7 @@ 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 empty if false.+ 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.@@ -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,52 @@ # 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
doctemplates.cabal view
@@ -1,5 +1,5 @@ name: doctemplates-version: 0.9+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@@ -225,8 +224,7 @@ Left (r :: Double) -> show r Right (i :: Integer) -> show i Bool b -> return $ BoolVal b- Object o -> MapVal . Context . M.fromList . H.toList <$>- mapM parseJSON o+ Object _ -> MapVal . Context <$> parseJSON v _ -> return NullVal instance TemplateTarget a => FromJSON (Context a) where@@ -236,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 b) -> return $ BoolVal b- _ -> 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 @@ -268,16 +244,6 @@ toJSON (SimpleVal d) = toJSON $ toText $ DL.render Nothing d toJSON (BoolVal b) = toJSON b -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- toYAML (BoolVal b) = toYAML b- mapDoc :: TemplateTarget a => (Doc a -> Doc a) -> Val a -> Val a mapDoc f val = case val of@@ -287,6 +253,11 @@ 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)@@ -332,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@@ -368,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)@@ -396,20 +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"]- BoolVal True -> ["true"]- BoolVal False -> []- 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@@ -446,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@@ -455,11 +465,12 @@ => Template a -> Context a -> RenderState (Doc a) renderTemp (Literal t) _ = updateColumn t renderTemp (Interpolate v) ctx =- updateColumn $ mconcat $ resolveVariable v ctx+ case resolveVariable v ctx of+ Resolved _ xs -> updateColumn (mconcat xs) renderTemp (Conditional v ift elset) ctx = case resolveVariable v ctx of- [] -> renderTemp elset ctx- _ -> renderTemp ift ctx+ 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
@@ -8,6 +8,6 @@ $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: