doctemplates-0.11/0000755000000000000000000000000007346545000012310 5ustar0000000000000000doctemplates-0.11/LICENSE0000644000000000000000000000277507346545000013330 0ustar0000000000000000Copyright John MacFarlane (c) 2009-2019 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 of Author name here nor the names of other 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 AND CONTRIBUTORS "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 OWNER OR CONTRIBUTORS 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. doctemplates-0.11/README.md0000644000000000000000000003113207346545000013567 0ustar0000000000000000#( doctemplates [![CI tests](https://github.com/jgm/doctemplates/workflows/CI%20tests/badge.svg)](https://github.com/jgm/doctemplates/actions) This is the text templating system used by pandoc. Its basic function is to fill variables in a template. Variables are provided by a "context." Any instance of the `ToContext` typeclass (such as an aeson `Value`) can serve as the context, or a `Context` value can be constructed manually. Control structures are provided to test whether a variable has a non-blank value and to iterate over the items of a list. Partials---that is, subtemplates defined in different files---are supported. Pipes can be used to transform the values of variables or partials. The provided pipes make it possible to do list enumeration and tabular layout in templates. Templates are rendered to a doclayout `Doc` (which is polymorphic in the underlying string type). If `Doc` values are used in the context, rendered documents will be able to wrap flexibly on breaking spaces. This feature makes doctemplates more suitable than other template engines for plain-text formats (like Markdown). Unlike the various HTML-centered template engines, doctemplates is output-format agnostic, so no automatic escaping is done on interpolated values. Values are assumed to be escaped properly in the Context. ## Example of use ``` haskell {-# LANGUAGE OverloadedStrings #-} import Data.Text (Text) import qualified Data.Text.IO as T import Data.Aeson import Text.DocTemplates import Text.DocLayout (render) data Employee = Employee { firstName :: String , lastName :: String , salary :: Maybe Int } instance ToJSON Employee where toJSON e = object [ "name" .= object [ "first" .= firstName e , "last" .= lastName e ] , "salary" .= salary e ] template :: Text template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$" main :: IO () main = do res <- compileTemplate "mytemplate.txt" template case res of Left e -> error e Right t -> T.putStrLn $ render Nothing $ renderTemplate t $ object ["employee" .= [ Employee "John" "Doe" Nothing , Employee "Omar" "Smith" (Just 30000) , Employee "Sara" "Chen" (Just 60000) ] ] ``` ## Delimiters To mark variables and control structures in the template, either `$`...`$` or `${`...`}` may be used as delimiters. The styles may also be mixed in the same template, but the opening and closing delimiter must match in each case. The opening delimiter may be followed by one or more spaces or tabs, which will be ignored. The closing delimiter may be followed by one or more spaces or tabs, which will be ignored. To include a literal `$` in the document, use `$$`. ## Comments Anything between the sequence `$--` and the end of the line will be treated as a comment and omitted from the output. ## Interpolated variables A slot for an interpolated variable is a variable name surrounded by matched delimiters. Variable names must begin with a letter and can contain letters, numbers, `_`, `-`, and `.`. The keywords `it`, `if`, `else`, `endif`, `for`, `sep`, and `endfor` may not be used as variable names. Examples: ``` $foo$ $foo.bar.baz$ $foo_bar.baz-bim$ $ foo $ ${foo} ${foo.bar.baz} ${foo_bar.baz-bim} ${ foo } ``` The values of variables are determined by the `Context` that is passed as a parameter to `renderTemplate`. So, for example, `title` will return the value of the `title` field, and `employee.salary` will return the value of the `salary` field of the object that is the value of the `employee` field. - If the value of the variable is simple value, it will be 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 empty 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. When a `Context` is derived from an aeson (JSON) `Value`, the following conversions are done: - If the value is a number, it will be rendered as an integer if possible, otherwise as a floating-point number. ## 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 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)$ $foo$ $endif$ $if(foo)$ part one $else$ part two $endif$ ${if(foo)}bar${endif} ${if(foo)} ${foo} ${endif} ${if(foo)} ${ foo.bar } ${else} no foo! ${endif} ``` The keyword `elseif` may be used to simplify complex nested conditionals. Thus ``` $if(foo)$ XXX $elseif(bar)$ YYY $else$ ZZZ $endif$ ``` is equivalent to ``` $if(foo)$ XXX $else$ $if(bar)$ YYY $else$ ZZZ $endif$ $endif$ ``` ## For loops A for loop begins with `for(variable)` (enclosed in matched delimiters) and ends with `endfor` (enclosed in matched delimiters. - If `variable` is an array, the material inside the loop will be evaluated repeatedly, with `variable` being set to each value of the array in turn, and concatenated. - If `variable` is a map, the material inside will be set to the map. - If the value of the associated variable is not an array or a map, a single iteration will be performed on its value. Examples: ``` $for(foo)$$foo$$sep$, $endfor$ $for(foo)$ - $foo.last$, $foo.first$ $endfor$ ${ for(foo.bar) } - ${ foo.bar.last }, ${ foo.bar.first } ${ endfor } $for(mymap)$ $it.name$: $it.office$ $endfor$ ``` You may optionally specify a separator between consecutive values using `sep` (enclosed in matched delimiters). The material between `sep` and the `endfor` is the separator. ``` ${ for(foo) }${ foo }${ sep }, ${ endfor } ``` Instead of using `variable` inside the loop, the special anaphoric keyword `it` may be used. ``` ${ for(foo.bar) } - ${ it.last }, ${ it.first } ${ endfor } ``` ## Partials Partials (subtemplates stored in different files) may be included using the syntax ``` ${ boilerplate() } ``` The partials are obtained using `getPartial` from the `TemplateMonad` class. This may be implemented differently in different monads. The path passed to `getPartial` is computed on the basis of the original template path (a parameter to `compileTemplate`) and the partial's name. The partial's name is substituted for the *base name* of the original template path (leaving the original template's extension), unless the partial has an explicit extension, in which case this is kept. So, with the `TemplateMonad` instance for IO, partials will be sought in the directory containing the main template, and will be assumed to have the extension of the main template. Partials may optionally be applied to variables using a colon: ``` ${ date:fancy() } ${ articles:bibentry() } ``` If `articles` is an array, this will iterate over its values, applying the partial `bibentry()` to each one. So the second example above is equivalent to ``` ${ for(articles) } ${ it:bibentry() } ${ endfor } ``` Note that the anaphoric keyword `it` must be used when iterating over partials. In the above examples, the `bibentry` partial should contain `it.title` (and so on) instead of `articles.title`. Final newlines are omitted from included partials. Partials may include other partials. If you exceed a nesting level of 50, though, in resolving partials, the literal `(loop)` will be returned, to avoid infinite loops. A separator between values of an array may be specified in square brackets, immediately after the variable name or partial: ``` ${months[, ]}$ ${articles:bibentry()[; ]$ ``` The separator in this case is literal and (unlike with `sep` in an explicit `for` loop) cannot contain interpolated variables or other template directives. ## Nesting To ensure that content is "nested," that is, subsequent lines indented, use the `^` directive: ``` $item.number$ $^$$item.description$ ($item.price$) ``` In this example, if `item.description` has multiple lines, they will all be indented to line up with the first line: ``` 00123 A fine bottle of 18-year old Oban whiskey. ($148) ``` To nest multiple lines to the same level, align them with the `^` directive in the template. For example: ``` $item.number$ $^$$item.description$ ($item.price$) (Available til $item.sellby$.) ``` will produce ``` 00123 A fine bottle of 18-year old Oban whiskey. ($148) (Available til March 30, 2020.) ``` If a variable occurs by itself on a line, preceded by whitespace and not followed by further text or directives on the same line, and the variable's value contains multiple lines, it will be nested automatically. ## Breakable spaces When rendering to a `Doc`, a distinction can be made between breakable and unbreakable spaces. Normally, spaces in the template itself (as opposed to values of the interpolated variables) are not breakable, but they can be made breakable in part of the template by using the `~` keyword (ended with another `~`). ``` $~$This long line may break if the document is rendered with a short line length.$~$ ``` The `~` keyword has no effect when rendering to `Text` or `String`. ## Pipes A pipe transforms the value of a variable or partial. Pipes are specified using a slash (`/`) between the variable name (or partial) and the pipe name. Example: ``` $for(name)$ $name/uppercase$ $endfor$ $for(metadata/pairs)$ - $it.key$: $it.value$ $endfor$ $employee:name()/uppercase$ ``` Pipes may be chained: ``` $for(employees/pairs)$ $it.key/alpha/uppercase$. $it.name$ $endfor$ ``` Some pipes take parameters: ``` |----------------------|------------| $for(employee)$ $it.name.first/uppercase/left 20 "| "$$it.name.salary/right 10 " | " " |"$ $endfor$ |----------------------|------------| ``` Currently the following pipes are predefined: - `pairs`: Converts a map or array to an array of maps, each with `key` and `value` fields. If the original value was an array, the `key` will be the array index, starting with 1. - `first`: Returns the first value of an array, if applied to a non-empty array; otherwise returns the original value. - `last`: Returns the last value of an array, if applied to a non-empty array; otherwise returns the original value. - `rest`: Returns all but the first value of an array, if applied to a non-empty array; otherwise returns the original value. - `allbutlast`: Returns all but the last value of an array, if applied to a non-empty array; otherwise returns the original value. - `uppercase`: Converts text to uppercase. - `lowercase`: Converts text to lowercase. - `length`: Returns the length of the value: number of characters for a textual value, number of elements for a map or array. - `reverse`: Reverses a textual value or array, and has no effect on other values. - `chomp`: Removes trailing newlines (and breakable space). - `nowrap`: Disables line wrapping on breakable spaces. - `alpha`: Converts textual values that can be read as an integer into lowercase alphabetic characters `a..z` (mod 26). This can be used to get lettered enumeration from array indices. To get uppercase letters, chain with `uppercase`. - `roman`: Converts textual values that can be read as an integer into lowercase roman numerials. This can be used to get lettered enumeration from array indices. To get uppercase roman, chain with `uppercase`. - `left n "leftborder" "rightborder"`: Renders a textual value in a block of width `n`, aligned to the left, with an optional left and right border. Has no effect on other values. This can be used to align material in tables. Widths are positive integers indicating the number of characters. Borders are strings inside double quotes; literal `"` and `\` characters must be backslash-escaped. - `right n "leftborder" "rightborder"`: Renders a textual value in a block of width `n`, aligned to the right, and has no effect on other values. - `center n "leftborder" "rightborder"`: Renders a textual value in a block of width `n`, aligned to the center, and has no effect on other values. doctemplates-0.11/Setup.hs0000644000000000000000000000005607346545000013745 0ustar0000000000000000import Distribution.Simple main = defaultMain doctemplates-0.11/bench/0000755000000000000000000000000007346545000013367 5ustar0000000000000000doctemplates-0.11/bench/bench.hs0000644000000000000000000000217507346545000015007 0ustar0000000000000000{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} import Text.DocTemplates import qualified Data.Text as T import Data.Text (Text) import Criterion.Main import Criterion.Types (Config (..)) import Control.Monad.Identity import Data.Semigroup ((<>)) import Data.Aeson (object, (.=), Value) import Text.DocLayout (render) main :: IO () main = do Right bigtextTemplate <- compileTemplate "bigtext.txt" bigtext defaultMainWith defaultConfig{ timeLimit = 5.0 } $ [ bench "applyTemplate" $ nf (fmap (render Nothing) . runIdentity . applyTemplate "bigtext" bigtext :: Value -> Either String Text) val , bench "renderTemplate" $ nf (render Nothing . renderTemplate bigtextTemplate :: Value -> Text) val ] bigtext :: Text bigtext = T.replicate 150 $ "Hello there $foo$. This is a big text.\n$for(bar)$$bar.baz$$endfor$\n" <> "$if(foo)$Hi $foo$.$endif$\n" val :: Value val = object [ "foo" .= (22 :: Int) , "bar" .= [ object [ "baz" .= ("Hello"::Text) ] , object [ "baz" .= ("Bye"::Text) ] ] ] doctemplates-0.11/changelog.md0000644000000000000000000002360007346545000014562 0ustar0000000000000000# doctemplates ## 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 always used for the variable in a loop, and in a nested loop there was no way to distinguish the value of the inner iteration from the value of the outer one. Now we assign the iterated value to both "it" and to the original variable name (e.g. "foo.bar"). This probably has a small negative performance impact. Note that this change also affects the output of the template parser: original variable names are now retained instead of being replaced by "it". * Remove duplicate IsString constraint (#14, Mario Lang). * Update haddocks from README (#10). * Minor code clean-ups (#7, favonia). * Add hsyaml >= 0.2 constraint (#6). ## 0.8.2 * Add filters: first, rest, last, allbutlast. * New constructors for Filter: FirstItem, LastItem, Rest, AllButLast [API change]. ## 0.8.1 * Depend on doclayout 0.3, which adds an additional method on the HasChars class. This fixes some stack overflows in rendering very long lines. ## 0.8 * Change `Filter` data type to `Pipe`. Use the nomenclature of "pipe" instead of "filter" to avoid confusion in pandoc between two notions of filter. Otherwise everything works the same. ## 0.7.2 * Add `nowrap` filter. * Improve `alpha`, `roman`, `uppercase`, `lowercase` filters so they apply recursively within a list or map. * Allow `for` loops to bind map value. In this case there is no iteration, but the anophoric variable 'it' is assigned, which may help in using filters that destructure a string into a map (if we add any). ## 0.7.1 * Add `chomp` filter. * Allow filters to be applied to output of partials. ## 0.7 * Add haddock Makefile target, which regenerates haddocks from README and tests the code example. * Remove `BreakingSpace` constructor on `Template`. Now we use doclayout `BreakingSpace` inside a `Literal`. * Add instance for `ToContext a (Doc a)`. * Get benchmarks compiling again. * Use (doclayout) `Doc` internally and for rendered output. + `TemplateTarget` is now a type constraint synonym, not a regular typeclass. + Constraint on `compileTemplate` and `applyTemplate` simplified using TemplateTarget. + DocTemplates reexports Text.DocLayout.Doc. + The `Literal` costructor of `Template` now takes a `Doc a` rather than an `a`. + The `SimpleVal` constructor of `Val` now takes a `Doc a` rather than an `a`. + `renderTemplate` now returns a `Doc a` rather than an `a`. (This value can be converted to an a using `render Nothing`.) * Remove fromText from `TemplateTarget`. Now we use `fromString` from Data.String. * Parameterize `Template` on underlying stringlike type. * Improved behavior of partials. * Improve indent functions: don't drop final newline. * Allow blank lines in nested section. * Indent for Text/String: don't indent empty lines. * Additional tests and documentation about nesting. * Render items in for loop before separator. Otherwise we throw off column calculation. * Remove `+-reflow`; replace with toggle `$~$`. * Remove pNewline parser; it isn't needed now. * Remove `+-nest`. * Fix nest parsing bug. * Improve nesting. + Change `Nested` constructor for `Template` so it doesn't take a parameter. + Nesting level is now determined dynamically at render time rather than at compile time. This gives much better results when nesting occurs after template directives. Benchmarks show a slight penalty in performance (from 3.5ms to 3.1ms in rendering), but it's not too much. * Add filters. Filters transform the value of a variable, e.g. changing a map into an array of key/value pairs. Closes #5. + Internal: Add `Filter` type and `[Filter]` parameter on `Variable`. + Remove `unVariable`; now we have `varParts` and `varFilters`. + Document filters in README.md. + Implement filters. + Add tests. * Add `ToYAML`, `FromYAML` instances for `Context`, `Val`. ## 0.6.2 * Remove unnecessary `TemplateTarget` constraints on `ToContext` instances. * Add `ToContext` instance for `Map Text a`. * Add `Data`, `Typeable` instances for `Context` and `Val`. ## 0.6.1 * Indent bare partials. ## 0.6 * Add `+nest`/`-nest` keywords. * Add `+reflow`/`-reflow` keywords. * Add Nested constructor to Template, remove Indented and Indented parameter for Interpolate. * More expansive description of library. ## 0.5.1 * Add elseif keyword. * Improve compile error source locations with partials. * Handle templates that don't end in newlines. Previously this caused problems in some cases. ## 0.5 * Add toText method to TemplateTarget class. * Add String and Lazy Text instances for TemplateTarget. * Swap Parameters in ToContext (so that the first parameter for both ToContext and FromContext refers to the parameter of Context). * Add toVal method to ToContext. * Default instance definition for toContext in terms of toVal, so that defining toVal is sufficient. * Add instances for ToContext and FromContext. * Remove valueToContext. Add ToJSON, FromJSON instances for Context and Val instead. * isEmpty: For Doc, treat `Text 0 _` as empty. Also `Concat x y` when x and y are empty. This differs from isEmpty in DocLayout itself, which only applies to Empty. * Code cleanup. ## 0.4 * Split into three modules. Main module only exports an opaque version of the Template type. Import Internal if you need to manipulate a Template. * Add Context type, parameterized on the underlying content's type. * Add Val type. * Add valueToContext for converting an Aeson Value to a Context. * Make renderTemplate and applyTemplate polymorphic in both context and target. Context parameter is now any instance of ToContext (instead of ToJSON). Result is now any instance of TemplateTarget. * Change type of getPartial in TemplateMonad so it runs in the TemplateMonad instance, not the Parser. Return a simple value rather than an Either; error handling can vary with the monad. * Remove TemplatePart. Template is now an algebraci data type, not a list of TemplateParts. * Add an Indented type to indicate indentation for interpolated variables. * Improve architecture, doing more at compile time. * Depend on doclayout. Context can be parameterized on a doclayout Doc type, allowing intelligent reflowing of content. * Remove single final newline in interpolated variable. * Remove final newline from partial. * Don't iterate when the variable evaluates to NullVal. * Only indented interpolated variables if by themselves on line. * Add Indented parameter to Interpolate constructor. * Update documentation and haddocks. * Add benchmark. ## 0.3.0.1 * Bump lower bound on base to 4.9, drop support for ghc 7.10. * Add needed import for older base versions. * Add test.hs to repository. ## 0.3 * Note that all of the changes to template syntax described below are backwards compatible, and all old pandoc templates should continue to work as before. * Allow `${...}` style delimiters around variables and directives, in addition to `$...$`. Allow space around the delimiters. * Support `$it$` as a variable for the current value in an iteration. (The old method, where the containing variable name is used, still works.) * Support partials (subtemplates defined in different files). * Interpolated array variables now have all elements rendered, concatenated, with an optional separator that can be specified using a new bracketed syntax. * Remove `TemplateTarget` class. It was pointless; the calling program can just do these trivial transformations. Avoids dependencies on bytestring, blaze-html, blaze-markup. * Change type of `renderTemplate` and `applyTemplate` to produce a `Text`, instead of being polymorphic. * Changed type of `compileTemplate`: it now takes a template path and the template contents, and returns either a template or an error. It runs in an instance of `TemplateMonad`, which is an abstraction around different ways of getting partials. (For example, in IO we can get partials by reading them from a file system, but in a web application one might want to obtain them from the database or have a set of them baked in.) * Remove `varListToJSON`. * Changed the architecture: `Template` is no longer just a newtype around a function, but a list of `TemplatePart`s. * Added a newtype for `Variable`. * Improved documentation in README.md. * Added a new test framework and much more extensive tests. doctemplates-0.11/doctemplates.cabal0000644000000000000000000000572107346545000015765 0ustar0000000000000000name: doctemplates version: 0.11 synopsis: Pandoc-style document templates description: This is the text templating system used by pandoc. It supports variable interpolation, iteration, tests for non-blank values, pipes, and partials. Templates are rendered to doclayout Docs, and variable values may come from a variety of different sources, including aeson Values. homepage: https://github.com/jgm/doctemplates#readme license: BSD3 license-file: LICENSE author: John MacFarlane maintainer: jgm@berkeley.edu copyright: 2016-19 John MacFarlane category: Text build-type: Simple -- extra-source-files: data-files: README.md changelog.md extra-source-files: test/*.test test/*.txt test/*.tex cabal-version: >=1.10 library hs-source-dirs: src exposed-modules: Text.DocTemplates Text.DocTemplates.Parser Text.DocTemplates.Internal build-depends: base >= 4.9 && < 5, safe, text-conversions, aeson, text, doclayout >= 0.4 && < 0.5, containers, vector, filepath, parsec, mtl, scientific if !impl(ghc >= 8.0) build-depends: semigroups == 0.18.* default-language: Haskell2010 ghc-options: -Wall -fno-warn-unused-do-bind test-suite doctemplates-test type: exitcode-stdio-1.0 hs-source-dirs: test main-is: test.hs build-depends: base, doctemplates, doclayout >= 0.4 && < 0.5, containers, aeson, Glob, tasty, tasty-golden, tasty-hunit, filepath, temporary, bytestring, text ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 benchmark doctemplates-bench Type: exitcode-stdio-1.0 Main-Is: bench.hs Hs-Source-Dirs: bench Build-Depends: doctemplates, doclayout >= 0.4 && < 0.5, base >= 4.8 && < 5, criterion >= 1.0, filepath, aeson, text, containers, mtl Ghc-Options: -rtsopts -Wall -fno-warn-unused-do-bind Default-Language: Haskell2010 source-repository head type: git location: https://github.com/jgm/doctemplates doctemplates-0.11/src/Text/0000755000000000000000000000000007346545000014023 5ustar0000000000000000doctemplates-0.11/src/Text/DocTemplates.hs0000644000000000000000000003512307346545000016747 0ustar0000000000000000{- | Module : Text.DocTemplates Copyright : Copyright (C) 2009-2019 John MacFarlane License : BSD3 Maintainer : John MacFarlane Stability : alpha Portability : portable This is the text templating system used by pandoc. Its basic function is to fill variables in a template. Variables are provided by a “context.” Any instance of the @ToContext@ typeclass (such as an aeson @Value@) can serve as the context, or a @Context@ value can be constructed manually. Control structures are provided to test whether a variable has a non-blank value and to iterate over the items of a list. Partials—that is, subtemplates defined in different files—are supported. Pipes can be used to transform the values of variables or partials. The provided pipes make it possible to do list enumeration and tabular layout in templates. Templates are rendered to a doclayout @Doc@ (which is polymorphic in the underlying string type). If @Doc@ values are used in the context, rendered documents will be able to wrap flexibly on breaking spaces. This feature makes doctemplates more suitable than other template engines for plain-text formats (like Markdown). Unlike the various HTML-centered template engines, doctemplates is output-format agnostic, so no automatic escaping is done on interpolated values. Values are assumed to be escaped properly in the Context. == Example of use > {-# LANGUAGE OverloadedStrings #-} > import Data.Text (Text) > import qualified Data.Text.IO as T > import Data.Aeson > import Text.DocTemplates > import Text.DocLayout (render) > > data Employee = Employee { firstName :: String > , lastName :: String > , salary :: Maybe Int } > instance ToJSON Employee where > toJSON e = object [ "name" .= object [ "first" .= firstName e > , "last" .= lastName e ] > , "salary" .= salary e ] > > template :: Text > template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$" > > main :: IO () > main = do > res <- compileTemplate "mytemplate.txt" template > case res of > Left e -> error e > Right t -> T.putStrLn $ render Nothing $ renderTemplate t $ object > ["employee" .= > [ Employee "John" "Doe" Nothing > , Employee "Omar" "Smith" (Just 30000) > , Employee "Sara" "Chen" (Just 60000) ] > ] == Delimiters To mark variables and control structures in the template, either @$@…@$@ or @${@…@}@ may be used as delimiters. The styles may also be mixed in the same template, but the opening and closing delimiter must match in each case. The opening delimiter may be followed by one or more spaces or tabs, which will be ignored. The closing delimiter may be followed by one or more spaces or tabs, which will be ignored. To include a literal @$@ in the document, use @$$@. == Comments Anything between the sequence @$--@ and the end of the line will be treated as a comment and omitted from the output. == Interpolated variables A slot for an interpolated variable is a variable name surrounded by matched delimiters. Variable names must begin with a letter and can contain letters, numbers, @_@, @-@, and @.@. The keywords @it@, @if@, @else@, @endif@, @for@, @sep@, and @endfor@ may not be used as variable names. Examples: > $foo$ > $foo.bar.baz$ > $foo_bar.baz-bim$ > $ foo $ > ${foo} > ${foo.bar.baz} > ${foo_bar.baz-bim} > ${ foo } The values of variables are determined by the @Context@ that is passed as a parameter to @renderTemplate@. So, for example, @title@ will return the value of the @title@ field, and @employee.salary@ will return the value of the @salary@ field of the object that is the value of the @employee@ field. - If the value of the variable is simple value, it will be 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 empty 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. When a @Context@ is derived from an aeson (JSON) @Value@, the following conversions are done: - If the value is a number, it will be rendered as an integer if possible, otherwise as a floating-point number. == 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 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)$ > $foo$ > $endif$ > > $if(foo)$ > part one > $else$ > part two > $endif$ > > ${if(foo)}bar${endif} > > ${if(foo)} > ${foo} > ${endif} > > ${if(foo)} > ${ foo.bar } > ${else} > no foo! > ${endif} The keyword @elseif@ may be used to simplify complex nested conditionals. Thus > $if(foo)$ > XXX > $elseif(bar)$ > YYY > $else$ > ZZZ > $endif$ is equivalent to > $if(foo)$ > XXX > $else$ > $if(bar)$ > YYY > $else$ > ZZZ > $endif$ > $endif$ == For loops A for loop begins with @for(variable)@ (enclosed in matched delimiters) and ends with @endfor@ (enclosed in matched delimiters. - If @variable@ is an array, the material inside the loop will be evaluated repeatedly, with @variable@ being set to each value of the array in turn, and concatenated. - If @variable@ is a map, the material inside will be set to the map. - If the value of the associated variable is not an array or a map, a single iteration will be performed on its value. Examples: > $for(foo)$$foo$$sep$, $endfor$ > > $for(foo)$ > - $foo.last$, $foo.first$ > $endfor$ > > ${ for(foo.bar) } > - ${ foo.bar.last }, ${ foo.bar.first } > ${ endfor } > > $for(mymap)$ > $it.name$: $it.office$ > $endfor$ You may optionally specify a separator between consecutive values using @sep@ (enclosed in matched delimiters). The material between @sep@ and the @endfor@ is the separator. > ${ for(foo) }${ foo }${ sep }, ${ endfor } Instead of using @variable@ inside the loop, the special anaphoric keyword @it@ may be used. > ${ for(foo.bar) } > - ${ it.last }, ${ it.first } > ${ endfor } == Partials Partials (subtemplates stored in different files) may be included using the syntax > ${ boilerplate() } The partials are obtained using @getPartial@ from the @TemplateMonad@ class. This may be implemented differently in different monads. The path passed to @getPartial@ is computed on the basis of the original template path (a parameter to @compileTemplate@) and the partial’s name. The partial’s name is substituted for the /base name/ of the original template path (leaving the original template’s extension), unless the partial has an explicit extension, in which case this is kept. So, with the @TemplateMonad@ instance for IO, partials will be sought in the directory containing the main template, and will be assumed to have the extension of the main template. Partials may optionally be applied to variables using a colon: > ${ date:fancy() } > > ${ articles:bibentry() } If @articles@ is an array, this will iterate over its values, applying the partial @bibentry()@ to each one. So the second example above is equivalent to > ${ for(articles) } > ${ it:bibentry() } > ${ endfor } Note that the anaphoric keyword @it@ must be used when iterating over partials. In the above examples, the @bibentry@ partial should contain @it.title@ (and so on) instead of @articles.title@. Final newlines are omitted from included partials. Partials may include other partials. If you exceed a nesting level of 50, though, in resolving partials, the literal @(loop)@ will be returned, to avoid infinite loops. A separator between values of an array may be specified in square brackets, immediately after the variable name or partial: > ${months[, ]}$ > > ${articles:bibentry()[; ]$ The separator in this case is literal and (unlike with @sep@ in an explicit @for@ loop) cannot contain interpolated variables or other template directives. == Nesting To ensure that content is “nested,” that is, subsequent lines indented, use the @^@ directive: > $item.number$ $^$$item.description$ ($item.price$) In this example, if @item.description@ has multiple lines, they will all be indented to line up with the first line: > 00123 A fine bottle of 18-year old > Oban whiskey. ($148) To nest multiple lines to the same level, align them with the @^@ directive in the template. For example: > $item.number$ $^$$item.description$ ($item.price$) > (Available til $item.sellby$.) will produce > 00123 A fine bottle of 18-year old > Oban whiskey. ($148) > (Available til March 30, 2020.) If a variable occurs by itself on a line, preceded by whitespace and not followed by further text or directives on the same line, and the variable’s value contains multiple lines, it will be nested automatically. == Breakable spaces When rendering to a @Doc@, a distinction can be made between breakable and unbreakable spaces. Normally, spaces in the template itself (as opposed to values of the interpolated variables) are not breakable, but they can be made breakable in part of the template by using the @~@ keyword (ended with another @~@). > $~$This long line may break if the document is rendered > with a short line length.$~$ The @~@ keyword has no effect when rendering to @Text@ or @String@. == Pipes A pipe transforms the value of a variable or partial. Pipes are specified using a slash (@\/@) between the variable name (or partial) and the pipe name. Example: > $for(name)$ > $name/uppercase$ > $endfor$ > > $for(metadata/pairs)$ > - $it.key$: $it.value$ > $endfor$ > > $employee:name()/uppercase$ Pipes may be chained: > $for(employees/pairs)$ > $it.key/alpha/uppercase$. $it.name$ > $endfor$ Some pipes take parameters: > |----------------------|------------| > $for(employee)$ > $it.name.first/uppercase/left 20 "| "$$it.name.salary/right 10 " | " " |"$ > $endfor$ > |----------------------|------------| Currently the following pipes are predefined: - @pairs@: Converts a map or array to an array of maps, each with @key@ and @value@ fields. If the original value was an array, the @key@ will be the array index, starting with 1. - @first@: Returns the first value of an array, if applied to a non-empty array; otherwise returns the original value. - @last@: Returns the last value of an array, if applied to a non-empty array; otherwise returns the original value. - @rest@: Returns all but the first value of an array, if applied to a non-empty array; otherwise returns the original value. - @allbutlast@: Returns all but the last value of an array, if applied to a non-empty array; otherwise returns the original value. - @uppercase@: Converts text to uppercase. - @lowercase@: Converts text to lowercase. - @length@: Returns the length of the value: number of characters for a textual value, number of elements for a map or array. - @reverse@: Reverses a textual value or array, and has no effect on other values. - @chomp@: Removes trailing newlines (and breakable space). - @nowrap@: Disables line wrapping on breakable spaces. - @alpha@: Converts textual values that can be read as an integer into lowercase alphabetic characters @a..z@ (mod 26). This can be used to get lettered enumeration from array indices. To get uppercase letters, chain with @uppercase@. - @roman@: Converts textual values that can be read as an integer into lowercase roman numerials. This can be used to get lettered enumeration from array indices. To get uppercase roman, chain with @uppercase@. - @left n \"leftborder\" \"rightborder\"@: Renders a textual value in a block of width @n@, aligned to the left, with an optional left and right border. Has no effect on other values. This can be used to align material in tables. Widths are positive integers indicating the number of characters. Borders are strings inside double quotes; literal @\"@ and @\\@ characters must be backslash-escaped. - @right n \"leftborder\" \"rightborder\"@: Renders a textual value in a block of width @n@, aligned to the right, and has no effect on other values. - @center n \"leftborder\" \"rightborder\"@: Renders a textual value in a block of width @n@, aligned to the center, and has no effect on other values. -} module Text.DocTemplates ( renderTemplate , compileTemplate , compileTemplateFile , applyTemplate , TemplateMonad(..) , TemplateTarget , Context(..) , Val(..) , ToContext(..) , FromContext(..) , Template -- export opaque type , Doc(..) ) where import qualified Data.Text.IO as TIO import Text.DocLayout (Doc(..)) import Data.Text (Text) import Text.DocTemplates.Parser (compileTemplate) import Text.DocTemplates.Internal ( TemplateMonad(..), Context(..), Val(..), ToContext(..), FromContext(..), TemplateTarget, Template, renderTemplate ) -- | Compile a template from a file. IO errors will be -- raised as exceptions; template parsing errors result in -- Left return values. compileTemplateFile :: TemplateTarget a => FilePath -> IO (Either String (Template a)) compileTemplateFile templPath = do templateText <- TIO.readFile templPath compileTemplate templPath templateText -- | Compile a template and apply it to a context. This is -- just a convenience function composing 'compileTemplate' -- and 'renderTemplate'. If a template will be rendered -- more than once in the same process, compile it separately -- for better performance. applyTemplate :: (TemplateMonad m, TemplateTarget a, ToContext a b) => FilePath -> Text -> b -> m (Either String (Doc a)) applyTemplate fp t val = do res <- compileTemplate fp t case res of Left s -> return $ Left s Right t' -> return $ Right $ renderTemplate t' val doctemplates-0.11/src/Text/DocTemplates/0000755000000000000000000000000007346545000016407 5ustar0000000000000000doctemplates-0.11/src/Text/DocTemplates/Internal.hs0000644000000000000000000003521607346545000020526 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveTraversable #-} {- | Module : Text.DocTemplates.Internal Copyright : Copyright (C) 2009-2019 John MacFarlane License : BSD3 Maintainer : John MacFarlane Stability : alpha Portability : portable -} module Text.DocTemplates.Internal ( renderTemplate , TemplateMonad(..) , Context(..) , Val(..) , ToContext(..) , FromContext(..) , TemplateTarget , Template(..) , Variable(..) , Pipe(..) , Alignment(..) , Border(..) ) where import Data.Text.Conversions (FromText(..), ToText(..)) import Data.Aeson (Value(..), ToJSON(..), FromJSON(..), Result(..), fromJSON) 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 import Text.DocLayout (Doc, HasChars) import qualified Text.DocLayout as DL import Data.String (IsString(..)) import Data.Data (Data) import Data.Typeable (Typeable) import GHC.Generics (Generic) import Data.Text (Text) import qualified Data.Map as M import qualified Data.Vector as V import Data.Scientific (floatingOrInteger) import Data.List (intersperse) #if MIN_VERSION_base(4,11,0) #else import Data.Semigroup #endif -- | A template. data Template a = Interpolate Variable | Conditional Variable (Template a) (Template a) | Iterate Variable (Template a) (Template a) | Nested (Template a) | Partial [Pipe] (Template a) | Literal (Doc a) | Concat (Template a) (Template a) | Empty deriving (Show, Read, Data, Typeable, Generic, Eq, Ord, Foldable, Traversable, Functor) instance Semigroup a => Semigroup (Template a) where x <> Empty = x Empty <> x = x x <> y = Concat x y instance Semigroup a => Monoid (Template a) where mappend = (<>) mempty = Empty data Pipe = ToPairs | ToUppercase | ToLowercase | ToLength | Reverse | FirstItem | LastItem | Rest | AllButLast | Chomp | ToAlpha | ToRoman | NoWrap | Block Alignment Int Border deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) data Alignment = LeftAligned | Centered | RightAligned deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) data Border = Border { borderLeft :: Text , borderRight :: Text } deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) -- | A variable which may have several parts (@foo.bar.baz@). data Variable = Variable { varParts :: [Text] , varPipes :: [Pipe] } deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) instance Semigroup Variable where Variable xs fs <> Variable ys gs = Variable (xs <> ys) (fs <> gs) instance Monoid Variable where mempty = Variable mempty mempty mappend = (<>) type TemplateTarget 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) } deriving (Show, Semigroup, Monoid, Traversable, Foldable, Functor, Data, Typeable) -- | A variable value. data Val a = SimpleVal (Doc a) | ListVal [Val a] | MapVal (Context a) | BoolVal Bool | NullVal deriving (Show, Traversable, Foldable, Functor, Data, Typeable) -- | The 'ToContext' class provides automatic conversion to -- a 'Context' or 'Val'. class ToContext a b where toContext :: b -> Context a toContext x = case toVal x of MapVal c -> c _ -> mempty toVal :: b -> Val a instance ToContext a (Context a) where toContext = id toVal = MapVal instance ToContext a (Val a) where toVal = id instance TemplateTarget a => ToContext a a where toVal = SimpleVal . DL.literal instance ToContext a a => ToContext a (Doc a) 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 = BoolVal True toVal False = BoolVal False instance TemplateTarget a => ToContext a Value where toContext x = case fromJSON x of Success y -> y Error _ -> mempty toVal x = case fromJSON x of Success y -> y Error _ -> NullVal -- | The 'FromContext' class provides functions for extracting -- values from 'Val' and 'Context'. class FromContext a b where fromVal :: Val a -> Maybe b lookupContext :: Text -> Context a -> Maybe b lookupContext t (Context m) = M.lookup t m >>= fromVal instance TemplateTarget a => FromContext a (Val a) where fromVal = Just instance TemplateTarget a => FromContext a (Doc a) where fromVal (SimpleVal x) = Just x fromVal _ = Nothing instance TemplateTarget a => FromContext a a where fromVal (SimpleVal x) = Just (DL.render Nothing x) fromVal _ = Nothing instance {-# OVERLAPPING #-} TemplateTarget [a] => FromContext [a] [a] where fromVal (SimpleVal x) = Just (DL.render Nothing x) fromVal _ = Nothing instance FromContext a b => FromContext a [b] where fromVal (ListVal xs) = mapM fromVal xs fromVal x = sequence [fromVal x] instance TemplateTarget a => FromJSON (Val a) where parseJSON v = case v of Array vec -> ListVal <$> mapM parseJSON (V.toList vec) String t -> return $ SimpleVal $ DL.literal $ fromText t Number n -> return $ SimpleVal $ fromString $ case floatingOrInteger n of Left (r :: Double) -> show r Right (i :: Integer) -> show i Bool b -> return $ BoolVal b Object _ -> MapVal . Context <$> parseJSON v _ -> return NullVal instance TemplateTarget a => FromJSON (Context a) where parseJSON v = do val <- parseJSON v case val of MapVal o -> return o _ -> fail "Expecting MapVal" instance TemplateTarget a => ToJSON (Context a) where toJSON (Context m) = toJSON m instance TemplateTarget a => ToJSON (Val a) where toJSON NullVal = Null toJSON (MapVal m) = toJSON m toJSON (ListVal xs) = toJSON xs toJSON (SimpleVal d) = toJSON $ toText $ DL.render Nothing d toJSON (BoolVal b) = toJSON b mapDoc :: TemplateTarget a => (Doc a -> Doc a) -> Val a -> Val a mapDoc f val = case val of 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 mapText :: TemplateTarget a => (Text -> Text) -> Val a -> Val a mapText f val = runIdentity (traverse (return . fromText . f . toText) val) applyPipe :: TemplateTarget a => Pipe -> Val a -> Val a applyPipe ToLength val = SimpleVal $ fromString . show $ len where len = case val of 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 applyPipe ToPairs val = case val of MapVal (Context m) -> ListVal $ map toPair $ M.toList m ListVal xs -> ListVal $ map toPair $ zip (map (fromString . show) [(1::Int)..]) xs _ -> val where toPair (k, v) = MapVal $ Context $ M.fromList [ ("key", SimpleVal $ fromString . T.unpack $ k) , ("value", v) ] applyPipe FirstItem val = case val of ListVal (x:_) -> x _ -> val applyPipe LastItem val = case val of ListVal xs@(_:_) -> last xs _ -> val applyPipe Rest val = case val of ListVal (_:xs) -> ListVal xs _ -> val applyPipe AllButLast val = case val of ListVal xs@(_:_) -> ListVal (init xs) _ -> val applyPipe Reverse val = case val of ListVal xs -> ListVal (reverse xs) SimpleVal{} -> mapText T.reverse val _ -> 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 NoWrap val = mapDoc DL.nowrap val applyPipe (Block align n border) val = let constructor = case align of LeftAligned -> DL.lblock Centered -> DL.cblock RightAligned -> DL.rblock toBorder y = if T.null y then mempty else DL.vfill (fromText y) in case nullToSimple val of SimpleVal d -> SimpleVal $ toBorder (borderLeft border) <> constructor n d <> toBorder (borderRight border) _ -> val nullToSimple :: Monoid a => Val a -> Val a nullToSimple NullVal = SimpleVal mempty nullToSimple x = x -- | Convert number 0 < x < 4000 to lowercase roman numeral. toRoman :: Int -> Maybe Text toRoman x | x >= 1000 , x < 4000 = ("m" <>) <$> toRoman (x - 1000) | x >= 900 = ("cm" <>) <$> toRoman (x - 900) | x >= 500 = ("d" <>) <$> toRoman (x - 500) | x >= 400 = ("cd" <>) <$> toRoman (x - 400) | x >= 100 = ("c" <>) <$> toRoman (x - 100) | x >= 90 = ("xc" <>) <$> toRoman (x - 90) | x >= 50 = ("l" <>) <$> toRoman (x - 50) | x >= 40 = ("xl" <>) <$> toRoman (x - 40) | x >= 10 = ("x" <>) <$> toRoman (x - 10) | x == 9 = return "ix" | x >= 5 = ("v" <>) <$> toRoman (x - 5) | x == 4 = return "iv" | x >= 1 = ("i" <>) <$> toRoman (x - 1) | x == 0 = return "" | otherwise = Nothing applyPipes :: TemplateTarget a => [Pipe] -> Val a -> Val a applyPipes fs x = foldr applyPipe x $ reverse fs multiLookup :: TemplateTarget a => [Text] -> Val a -> Val a multiLookup [] x = x multiLookup (t:vs) (MapVal (Context o)) = case M.lookup t o of Nothing -> NullVal Just v' -> multiLookup vs v' multiLookup _ _ = NullVal -- 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 -> Resolved a resolveVariable' v val = case applyPipes (varPipes v) $ multiLookup (varParts v) val of ListVal xs -> mconcat $ map (resolveVariable' mempty) xs SimpleVal d | 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 removeFinalNl DL.CarriageReturn = mempty removeFinalNl (DL.Concat d1 d2) = d1 <> removeFinalNl d2 removeFinalNl x = x withVariable :: (Monad m, TemplateTarget a) => Variable -> Context a -> (Context a -> m (Doc a)) -> m [Doc a] withVariable var ctx f = case applyPipes (varPipes var) $ multiLookup (varParts var) (MapVal ctx) of NullVal -> return mempty ListVal xs -> mapM (\iterval -> f $ setVarVal iterval) xs MapVal ctx' -> (:[]) <$> f (setVarVal (MapVal ctx')) val' -> (:[]) <$> f (setVarVal val') where setVarVal x = addToContext var x $ Context $ M.insert "it" x $ unContext ctx addToContext (Variable [] _) _ (Context ctx') = Context ctx' addToContext (Variable (v:vs) fs) x (Context ctx') = Context $ M.adjust (\z -> case z of _ | null vs -> x MapVal m -> MapVal $ addToContext (Variable vs fs) x m _ -> z) v ctx' type RenderState = S.State Int -- | Render a compiled template in a "context" which provides -- values for the template's variables. renderTemplate :: (TemplateTarget a, ToContext a b) => Template a -> b -> Doc a renderTemplate t x = S.evalState (renderTemp t (toContext x)) 0 updateColumn :: TemplateTarget a => Doc a -> RenderState (Doc a) updateColumn x = do S.modify $ DL.updateColumn x return x renderTemp :: forall a . TemplateTarget a => Template a -> Context a -> RenderState (Doc a) renderTemp (Literal t) _ = updateColumn t renderTemp (Interpolate v) ctx = case resolveVariable v ctx of Resolved _ xs -> updateColumn (mconcat xs) renderTemp (Conditional v ift elset) 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 return . mconcat . intersperse sep' $ xs renderTemp (Nested t) ctx = do n <- S.get DL.nest n <$> renderTemp t ctx renderTemp (Partial fs t) ctx = do val' <- renderTemp t ctx return $ case applyPipes fs (SimpleVal val') of SimpleVal x -> x _ -> mempty renderTemp (Concat t1 t2) ctx = mappend <$> renderTemp t1 ctx <*> renderTemp t2 ctx renderTemp Empty _ = return mempty -- | A 'TemplateMonad' defines a function to retrieve a partial -- (from the file system, from a database, or using a default -- value). class Monad m => TemplateMonad m where getPartial :: FilePath -> m Text instance TemplateMonad Identity where getPartial _ = return mempty instance TemplateMonad IO where getPartial = TIO.readFile doctemplates-0.11/src/Text/DocTemplates/Parser.hs0000644000000000000000000003355007346545000020205 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.DocTemplates.Parser Copyright : Copyright (C) 2009-2019 John MacFarlane License : BSD3 Maintainer : John MacFarlane Stability : alpha Portability : portable -} module Text.DocTemplates.Parser ( compileTemplate ) where import Data.Char (isAlphaNum) import Control.Monad (guard, when) import Control.Monad.Trans (lift) import qualified Text.Parsec as P import qualified Text.Parsec.Pos as P import Control.Applicative import Data.String (IsString(..)) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Read as T import System.FilePath import Text.DocTemplates.Internal import qualified Text.DocLayout as DL #if MIN_VERSION_base(4,11,0) #else import Data.Semigroup ((<>), Semigroup) #endif -- | Compile a template. The FilePath parameter is used -- to determine a default path and extension for partials -- and may be left empty if partials are not used. compileTemplate :: (TemplateMonad m, TemplateTarget a) => FilePath -> Text -> m (Either String (Template a)) compileTemplate templPath template = do res <- P.runParserT (pTemplate <* P.eof) PState{ templatePath = templPath , partialNesting = 1 , breakingSpaces = False , firstNonspace = P.initialPos templPath , nestedCol = Nothing , insideDirective = False } templPath template case res of Left e -> return $ Left $ show e Right x -> return $ Right x data PState = PState { templatePath :: FilePath , partialNesting :: !Int , breakingSpaces :: !Bool , firstNonspace :: P.SourcePos , nestedCol :: Maybe Int , insideDirective :: Bool } type Parser = P.ParsecT Text PState pTemplate :: (TemplateMonad m, TemplateTarget a) => Parser m (Template a) pTemplate = do P.skipMany pComment mconcat <$> many ((pLit <|> pNewline <|> pDirective <|> pEscape) <* P.skipMany pComment) pEndline :: Monad m => Parser m String pEndline = P.try $ do nls <- pLineEnding mbNested <- nestedCol <$> P.getState inside <- insideDirective <$> P.getState case mbNested of Just col -> do P.skipMany $ do P.getPosition >>= guard . (< col) . P.sourceColumn P.char ' ' <|> P.char '\t' curcol <- P.sourceColumn <$> P.getPosition guard $ inside || curcol >= col Nothing -> return () return nls pBlankLine :: (TemplateTarget a, Monad m) => Parser m (Template a) pBlankLine = P.try $ Literal . fromString <$> pLineEnding <* P.lookAhead pNewlineOrEof pNewline :: (TemplateTarget a, Monad m) => Parser m (Template a) pNewline = P.try $ do nls <- pEndline sps <- P.many (P.char ' ' <|> P.char '\t') breakspaces <- breakingSpaces <$> P.getState pos <- P.getPosition P.updateState $ \st -> st{ firstNonspace = pos } return $ Literal $ if breakspaces then DL.BreakingSpace else fromString $ nls <> sps pLit :: (TemplateTarget a, Monad m) => Parser m (Template a) pLit = do cs <- P.many1 (P.satisfy (\c -> c /= '$' && c /= '\n' && c /= '\r')) when (all (\c -> c == ' ' || c == '\t') cs) $ do pos <- P.getPosition when (P.sourceLine pos == 1) $ P.updateState $ \st -> st{ firstNonspace = pos } breakspaces <- breakingSpaces <$> P.getState if breakspaces then return $ toBreakable cs else return $ Literal $ fromString cs toBreakable :: TemplateTarget a => String -> Template a toBreakable [] = Empty toBreakable xs = case break isSpacy xs of ([], []) -> Empty ([], zs) -> Literal DL.BreakingSpace <> toBreakable (dropWhile isSpacy zs) (ys, []) -> Literal (fromString ys) (ys, zs) -> Literal (fromString ys) <> toBreakable zs isSpacy :: Char -> Bool isSpacy ' ' = True isSpacy '\n' = True isSpacy '\r' = True isSpacy '\t' = True isSpacy _ = False backupSourcePos :: Monad m => Int -> Parser m () backupSourcePos n = do pos <- P.getPosition P.setPosition $ P.incSourceColumn pos (- n) pEscape :: (TemplateTarget a, Monad m) => Parser m (Template a) pEscape = Literal "$" <$ P.try (P.string "$$" <* backupSourcePos 1) pDirective :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pDirective = pConditional <|> pForLoop <|> pReflowToggle <|> pNested <|> pInterpolate <|> pBarePartial pEnclosed :: Monad m => Parser m a -> Parser m a pEnclosed parser = P.try $ do closer <- pOpen P.skipMany pSpaceOrTab result <- parser P.skipMany pSpaceOrTab closer return result pParens :: Monad m => Parser m a -> Parser m a pParens parser = do P.char '(' result <- parser P.char ')' return result pInside :: Monad m => Parser m (Template a) -> Parser m (Template a) pInside parser = do oldInside <- insideDirective <$> P.getState P.updateState $ \st -> st{ insideDirective = True } res <- parser P.updateState $ \st -> st{ insideDirective = oldInside } return res pConditional :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pConditional = do v <- pEnclosed $ P.try $ P.string "if" *> pParens pVar pInside $ do multiline <- P.option False (True <$ skipEndline) -- if newline after the "if", then a newline after "endif" will be swallowed ifContents <- pTemplate elseContents <- P.option mempty (pElse multiline <|> pElseIf) pEnclosed (P.string "endif") when multiline $ P.option () skipEndline return $ Conditional v ifContents elseContents pElse :: (TemplateTarget a, TemplateMonad m) => Bool -> Parser m (Template a) pElse multiline = do pEnclosed (P.string "else") when multiline $ P.option () skipEndline pTemplate pElseIf :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pElseIf = do v <- pEnclosed $ P.try $ P.string "elseif" *> pParens pVar multiline <- P.option False (True <$ skipEndline) ifContents <- pTemplate elseContents <- P.option mempty (pElse multiline <|> pElseIf) return $ Conditional v ifContents elseContents skipEndline :: Monad m => Parser m () skipEndline = do pEndline pos <- P.lookAhead $ do P.skipMany (P.char ' ' <|> P.char '\t') P.getPosition P.updateState $ \st -> st{ firstNonspace = pos } pReflowToggle :: (Monoid a, Semigroup a, TemplateMonad m) => Parser m (Template a) pReflowToggle = do pEnclosed $ P.char '~' P.modifyState $ \st -> st{ breakingSpaces = not (breakingSpaces st) } return mempty pNested :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pNested = do col <- P.sourceColumn <$> P.getPosition pEnclosed $ P.char '^' oldNested <- nestedCol <$> P.getState P.updateState $ \st -> st{ nestedCol = Just col } x <- pTemplate xs <- P.many $ P.try $ do y <- mconcat <$> P.many1 pBlankLine z <- pTemplate return (y <> z) let contents = x <> mconcat xs P.updateState $ \st -> st{ nestedCol = oldNested } return $ Nested contents pForLoop :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pForLoop = do v <- pEnclosed $ P.try $ P.string "for" *> pParens pVar -- if newline after the "for", then a newline after "endfor" will be swallowed pInside $ do multiline <- P.option False $ skipEndline >> return True contents <- pTemplate sep <- P.option mempty $ do pEnclosed (P.string "sep") when multiline $ P.option () skipEndline pTemplate pEnclosed (P.string "endfor") when multiline $ P.option () skipEndline return $ Iterate v contents sep pInterpolate :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pInterpolate = do pos <- P.getPosition -- we don't used pEnclosed here, to get better error messages: (closer, var) <- P.try $ do cl <- pOpen P.skipMany pSpaceOrTab v <- pVar P.notFollowedBy (P.char '(') -- bare partial return (cl, v) res <- (P.char ':' *> (pPartialName >>= pPartial (Just var))) <|> Iterate var (Interpolate (Variable ["it"] [])) <$> pSep <|> return (Interpolate var) P.skipMany pSpaceOrTab closer handleNesting False pos res pLineEnding :: Monad m => Parser m String pLineEnding = P.string "\n" <|> P.try (P.string "\r\n") <|> P.string "\r" pNewlineOrEof :: Monad m => Parser m () pNewlineOrEof = () <$ pLineEnding <|> P.eof handleNesting :: TemplateMonad m => Bool -> P.SourcePos -> Template a -> Parser m (Template a) handleNesting eatEndline pos templ = do firstNonspacePos <- firstNonspace <$> P.getState let beginline = firstNonspacePos == pos endofline <- (True <$ P.lookAhead pNewlineOrEof) <|> pure False when (eatEndline && beginline) $ P.optional skipEndline mbNested <- nestedCol <$> P.getState let toNested t@(Nested{}) = t toNested t = case P.sourceColumn pos of 1 -> t n | Just n == mbNested -> t | otherwise -> Nested t return $ if beginline && endofline then toNested templ else templ pBarePartial :: (TemplateTarget a, TemplateMonad m) => Parser m (Template a) pBarePartial = do pos <- P.getPosition (closer, fp) <- P.try $ do closer <- pOpen P.skipMany pSpaceOrTab fp <- pPartialName return (closer, fp) res <- pPartial Nothing fp P.skipMany pSpaceOrTab closer handleNesting True pos res pPartialName :: TemplateMonad m => Parser m FilePath pPartialName = P.try $ do fp <- P.many1 (P.alphaNum <|> P.oneOf ['_','-','.','/','\\']) P.string "()" return fp pPartial :: (TemplateTarget a, TemplateMonad m) => Maybe Variable -> FilePath -> Parser m (Template a) pPartial mbvar fp = do oldst <- P.getState separ <- P.option mempty pSep tp <- templatePath <$> P.getState let fp' = case takeExtension fp of "" -> replaceBaseName tp fp _ -> replaceFileName tp fp partial <- lift $ removeFinalNewline <$> getPartial fp' nesting <- partialNesting <$> P.getState t <- if nesting > 50 then return $ Literal "(loop)" else do oldInput <- P.getInput oldPos <- P.getPosition P.setPosition $ P.initialPos fp' P.setInput partial P.updateState $ \st -> st{ partialNesting = nesting + 1 } P.updateState $ \st -> st{ nestedCol = Nothing } res' <- pTemplate <* P.eof P.updateState $ \st -> st{ partialNesting = nesting } P.setInput oldInput P.setPosition oldPos return res' P.putState oldst fs <- many pPipe case mbvar of Just var -> return $ Iterate var (Partial fs t) separ Nothing -> return $ Partial fs t removeFinalNewline :: Text -> Text removeFinalNewline t = case T.unsnoc t of Just (t', '\n') -> t' _ -> t pSep :: (TemplateTarget a, Monad m) => Parser m (Template a) pSep = do P.char '[' xs <- P.many (P.satisfy (/= ']')) P.char ']' return $ Literal (fromString xs) pSpaceOrTab :: Monad m => Parser m Char pSpaceOrTab = P.satisfy (\c -> c == ' ' || c == '\t') pComment :: Monad m => Parser m () pComment = do pos <- P.getPosition P.try (P.string "$--") P.skipMany (P.satisfy (/='\n')) -- If the comment begins in the first column, the line ending -- will be consumed; otherwise not. when (P.sourceColumn pos == 1) $ () <$ pNewlineOrEof pOpenDollar :: Monad m => Parser m (Parser m ()) pOpenDollar = pCloseDollar <$ P.try (P.char '$' <* P.notFollowedBy (P.char '$' <|> P.char '{')) where pCloseDollar = () <$ P.char '$' pOpenBraces :: Monad m => Parser m (Parser m ()) pOpenBraces = pCloseBraces <$ P.try (P.string "${" <* P.notFollowedBy (P.char '}')) where pCloseBraces = () <$ P.try (P.char '}') pOpen :: Monad m => Parser m (Parser m ()) pOpen = pOpenDollar <|> pOpenBraces pVar :: Monad m => Parser m Variable pVar = do first <- pIdentPart <|> pIt rest <- P.many (P.char '.' *> pIdentPart) pipes <- P.many pPipe return $ Variable (first:rest) pipes pPipe :: Monad m => Parser m Pipe pPipe = do P.char '/' pipeName <- P.many1 P.letter P.notFollowedBy P.letter case pipeName of "uppercase" -> return ToUppercase "lowercase" -> return ToLowercase "pairs" -> return ToPairs "length" -> return ToLength "alpha" -> return ToAlpha "roman" -> return ToRoman "reverse" -> return Reverse "first" -> return FirstItem "rest" -> return Rest "last" -> return LastItem "allbutlast" -> return AllButLast "chomp" -> return Chomp "nowrap" -> return NoWrap "left" -> Block LeftAligned <$> pBlockWidth <*> pBlockBorders "right" -> Block RightAligned <$> pBlockWidth <*> pBlockBorders "center" -> Block Centered <$> pBlockWidth <*> pBlockBorders _ -> fail $ "Unknown pipe " ++ pipeName pBlockWidth :: Monad m => Parser m Int pBlockWidth = P.try (do _ <- P.many1 P.space ds <- P.many1 P.digit case T.decimal (T.pack ds) of Right (n,"") -> return n _ -> fail "Expected integer parameter for pipe") P. "integer parameter for pipe" pBlockBorders :: Monad m => Parser m Border pBlockBorders = do P.skipMany P.space let pBorder = do P.char '"' cs <- P.many $ (P.noneOf ['"','\\']) <|> (P.char '\\' >> P.anyChar) P.char '"' P.skipMany P.space return $ T.pack cs Border <$> P.option mempty pBorder <*> P.option mempty pBorder pIt :: Monad m => Parser m Text pIt = fromString <$> P.try (P.string "it") pIdentPart :: Monad m => Parser m Text pIdentPart = P.try $ do first <- P.letter rest <- P.many (P.satisfy (\c -> isAlphaNum c || c == '_' || c == '-')) let part = first : rest guard $ part `notElem` reservedWords return $ fromString part reservedWords :: [String] reservedWords = ["if","else","endif","elseif","for","endfor","sep","it"] doctemplates-0.11/test/0000755000000000000000000000000007346545000013267 5ustar0000000000000000doctemplates-0.11/test/bad.txt0000644000000000000000000000003307346545000014552 0ustar0000000000000000partial $with syntax error doctemplates-0.11/test/basic-with-braces.test0000644000000000000000000000071207346545000017457 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . ${ for(employee) } Hi, ${employee.name.first}. ${ if(employee.salary) }You make $$${ employee.salary }.${ else }No salary data.${ endif } ${ endfor } . Hi, John. No salary data. Hi, Omar. You make $30000. Hi, Sara. You make $60000. doctemplates-0.11/test/basic-with-it.test0000644000000000000000000000064507346545000016641 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $for(employee)$ Hi, $it.name.first$. $if(it.salary)$You make $$$it.salary$.$else$No salary data.$endif$ $endfor$ . Hi, John. No salary data. Hi, Omar. You make $30000. Hi, Sara. You make $60000. doctemplates-0.11/test/basic.test0000644000000000000000000000067107346545000015255 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $for(employee)$ Hi, $employee.name.first$. $if(employee.salary)$You make $$$employee.salary$.$else$No salary data.$endif$ $ endfor $ . Hi, John. No salary data. Hi, Omar. You make $30000. Hi, Sara. You make $60000. doctemplates-0.11/test/boilerplate.txt0000644000000000000000000000002207346545000016324 0ustar0000000000000000BOILERPLATE HERE doctemplates-0.11/test/boolean.test0000644000000000000000000000017507346545000015612 0ustar0000000000000000{ "foo": true, "bar": false } . $foo$ $bar$ $if(foo)$XXX$else$YYY$endif$ $if(bar)$XXX$else$YYY$endif$ . true false XXX YYY doctemplates-0.11/test/comments.test0000644000000000000000000000015207346545000016013 0ustar0000000000000000{ "foo": 3 } . $-- a comment $--${foo} more comment $$-- not a comment a$-- comment . $-- not a comment a doctemplates-0.11/test/conditionals.test0000644000000000000000000000053407346545000016660 0ustar0000000000000000{ "foo": 1, "bar": null, "baz": ["a", "b"], "bim": { "zub": "sim" }, "sup": [ { "biz": "qux" } , { "sax": "" } ] } . ${if(sup.sax)} XXX ${else} YYY ${endif} ${if(bar)} BAR ${endif} ${if(bar)}BAR${endif} ${if(foo)} FOO ${endif} ${if(baz)} BAZ ${endif} ${if(bim)} BIM ${endif} ${if(sup)} SUP ${endif} . YYY FOO BAZ BIM SUP doctemplates-0.11/test/elseif.test0000644000000000000000000000045007346545000015436 0ustar0000000000000000{ "foo": 1, "bar": null, "baz": ["a", "b"], "bim": { "zub": "sim" }, "sup": [ { "biz": "qux" } , { "sax": "" } ] } . $if(sup.sax)$ XXX $elseif(baz)$ YYY $else$ ZZZ $endif$ $if(sup.sax)$ XXX $elseif(baz.nonexist)$ YYY $elseif(sup.sax)$ ZZZ $else$ WWW $endif$ . YYY WWW doctemplates-0.11/test/empty.test0000644000000000000000000000065507346545000015334 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": [] } ] } . $for(employee)$ Hi, $employee.name.first$. $if(employee.salary)$You make $$$employee.salary$.$else$No salary data.$endif$ $ endfor $ . Hi, John. No salary data. Hi, Omar. No salary data. Hi, Sara. No salary data. doctemplates-0.11/test/enum.txt0000644000000000000000000000005207346545000014771 0ustar0000000000000000$it.key/alpha/uppercase$. $^$$it.value$ doctemplates-0.11/test/final-newline.test0000644000000000000000000000023607346545000016721 0ustar0000000000000000{ "employee": [ { "name": "John\n" } , { "name": "Sara\n\n" } , { "name": "Omar" } ] } . $for(employee)$ $employee.name$ $ endfor $ . John Sara Omar doctemplates-0.11/test/forloop.test0000644000000000000000000000062007346545000015646 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } , "salary": "30000" } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "60000" } , { "name": { "first": "Sara", "last": "Chen" } } ] } . $for(employee)$ $employee.name.first$ $employee.name.last$$sep$; $endfor$ $for(employee)$$employee.salary$$sep$; $endfor$ . John Doe; Omar Smith; Sara Chen 30000; 60000; doctemplates-0.11/test/indent.test0000644000000000000000000000043007346545000015446 0ustar0000000000000000{ "foo": "FOO1\nFOO2" , "bar": [ "LINE1\n LINE2" , "LINE3\n LINE4" ] } . $if(foo)$ $foo$ $endif$ $for(bar)$ $bar$ $endfor$ - $foo$ $foo$ - $if(foo)$$foo$$endif$ . FOO1 FOO2 LINE1 LINE2 LINE3 LINE4 - FOO1 FOO2 FOO1 FOO2 - FOO1 FOO2 doctemplates-0.11/test/inparens.txt0000644000000000000000000000000707346545000015644 0ustar0000000000000000($it$) doctemplates-0.11/test/loop-in-object.test0000644000000000000000000000062407346545000017013 0ustar0000000000000000{ "worksite": { "name": "canyon" , "workers": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } } . ${ for(worksite.workers) } ${it.name.last}, ${it.name.first} ${ endfor } . Doe, John Smith, Omar Chen, Sara doctemplates-0.11/test/loop-in-partial.test0000644000000000000000000000003107346545000017171 0ustar0000000000000000{} . $loop1()$ . (loop) doctemplates-0.11/test/loop1.txt0000644000000000000000000000001207346545000015053 0ustar0000000000000000$loop2()$ doctemplates-0.11/test/loop2.txt0000644000000000000000000000001207346545000015054 0ustar0000000000000000$loop1()$ doctemplates-0.11/test/name.tex0000644000000000000000000000004707346545000014732 0ustar0000000000000000\name{$it.name.first$}{$it.name.last$} doctemplates-0.11/test/name.txt0000644000000000000000000000005207346545000014745 0ustar0000000000000000$it.name.first:inparens()$ $it.name.last$ doctemplates-0.11/test/nest.test0000644000000000000000000000115207346545000015140 0ustar0000000000000000{ "foo": 1, "baz": ["a", "b"], "bim": { "zub": "sim" }, "sup": "a multiline\nstring" } . $sup$ $sup$ $^$$sup$ $bim.zub$ $^$$sup$ $bim.zub$ $^$$foo$ bar $sup$ $for(baz)$ 1. $^$Hello $if(it)$ $it$ $endif$ $endfor$ $^$hey $sup$ hey $sup$ hey $sup$ hey $if(foo)$ $foo$ $endif$ hey . a multiline string a multiline string a multiline string sim a multiline string sim 1 bar a multiline string 1. Hello a 1. Hello b hey a multiline string hey a multiline string hey a multiline string hey 1 hey doctemplates-0.11/test/nested-loop.test0000644000000000000000000000107107346545000016420 0ustar0000000000000000{ "pages": [ { "subpages": [ { "slug": "subpage-1" }, { "slug": "subpage-2" } ], "slug": "page-1" }, { "subpages": [ { "slug": "subpage-1" }, { "slug": "subpage-2" } ], "slug": "page-2" } ] } . $-- see #15 $for(pages)$ /$pages.slug$ $for(pages.subpages)$ /$pages.slug$/$pages.subpages.slug$ $endfor$ $endfor$ . /page-1 /page-1/subpage-1 /page-1/subpage-2 /page-2 /page-2/subpage-1 /page-2/subpage-2 doctemplates-0.11/test/numbers.test0000644000000000000000000000006007346545000015637 0ustar0000000000000000{ "m": 5 , "n": 7.3 } . $m$ and $n$ . 5 and 7.3 doctemplates-0.11/test/pad.test0000644000000000000000000000211707346545000014735 0ustar0000000000000000{ "sup": "a multiline\nstring", "baz": ["a\nb", "b\nc\nd"], "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $sup/right 15$$sup/center 15$$sup/left 15$ $for(baz/pairs)$ $it.key/alpha/right 4$. $^$$it.value$ $endfor$ +------+-----------+ $for(baz/pairs)$ $it.key/right 4 "| " " | "$$it.value/left 10 "" "|"$ +------+-----------+ $endfor$ |------------|------------| $for(employee)$ $it.name.first/uppercase/left 10 "| "$$it.salary/right 10 " | " " |"$ $endfor$ |------------|------------| . a multiline a multiline a multiline string string string a. a b b. b c d +------+-----------+ | 1 | a | | | b | +------+-----------+ | 2 | b | | | c | | | d | +------+-----------+ |------------|------------| | JOHN | | | OMAR | 30000 | | SARA | 60000 | |------------|------------| doctemplates-0.11/test/partial_foo.txt0000644000000000000000000000003607346545000016326 0ustar0000000000000000Hello $if(foo)$ $foo$ $endif$ doctemplates-0.11/test/partials.test0000644000000000000000000000110407346545000016003 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $for(employee)$ $it:name()$ $endfor$ $employee:name()[, ]$ $employee:name()$ $employee:name.tex()[; ]$ --- $boilerplate()$ --- $partial_foo()$ . (John) Doe (Omar) Smith (Sara) Chen (John) Doe, (Omar) Smith, (Sara) Chen (John) Doe(Omar) Smith(Sara) Chen \name{John}{Doe}; \name{Omar}{Smith}; \name{Sara}{Chen} --- BOILERPLATE HERE --- Hello doctemplates-0.11/test/pipe-and-partial.test0000644000000000000000000000046707346545000017326 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $for(employee)$ $it:name()/uppercase$ $endfor$ . (JOHN) DOE (OMAR) SMITH (SARA) CHEN doctemplates-0.11/test/pipes.test0000644000000000000000000000222307346545000015307 0ustar0000000000000000{ "foo": 1, "bar": null, "baz": ["a", "b"], "bim": { "Zub": "Sim" }, "sup": [ { "biz": "qux" }, { "sax": 2 } ], "items": [ "one with\na line break", "two", "three with\na line break" ], "hasblanks": "hello\n\n", "hasblanksmap": { "a": "hello\n\n", "b": "there\n\n" }, "digits": [1, 5, 20] } . $bar/length$ $baz/length$ $bim.Zub/length$ $bim/length$ $sup/length$ $baz/uppercase[, ]$ $for(baz)$ $it$ $it/uppercase$ $baz$ $baz/uppercase$ $endfor$ $for(bim/pairs)$ $it.key$: $it.value$ $bim.key$: $bim.value/lowercase$ $endfor$ $for(baz/pairs)$ $it.key/roman/uppercase/right 4$. $it.value$ $endfor$ $items/pairs/reverse:enum()$ ($hasblanks/chomp$) $for(hasblanksmap/chomp/pairs/uppercase)$ $it.key$ ($it.value$) $endfor$ $digits/roman[ ]$ $for(bim/uppercase)$ $it.Zub$ $endfor$ $digits/first$ $digits/last$ $for(digits/rest)$ $it$ $endfor$ $for(digits/allbutlast)$ $it$ $endfor$ $foo/first$ . 0 2 3 1 2 A, B a A a A b B b B Zub: Sim Zub: sim I. a II. b C. three with a line break B. two A. one with a line break (hello) A (HELLO) B (THERE) i v xx SIM 1 20 5 20 1 5 1 doctemplates-0.11/test/space-in-loop.test0000644000000000000000000000052307346545000016636 0ustar0000000000000000{ "employee": [ { "name": { "first": "John", "last": "Doe" } } , { "name": { "first": "Omar", "last": "Smith" } , "salary": "30000" } , { "name": { "first": "Sara", "last": "Chen" } , "salary": "60000" } ] } . $for(employee)$ $employee.name.first$ $endfor$ --- $for(nonexistent)$ $endfor$ --- . John Omar Sara --- --- doctemplates-0.11/test/test.hs0000644000000000000000000001121407346545000014601 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} import Text.DocLayout (render) import qualified Text.DocLayout as DL import qualified Data.Map as M import Text.DocTemplates import Test.Tasty.Golden import Test.Tasty import Test.Tasty.HUnit import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Encoding as T import System.FilePath import System.IO.Temp import Data.Aeson import System.FilePath.Glob import qualified Data.ByteString.Lazy as BL import Data.Semigroup ((<>)) import Data.Maybe main :: IO () main = withTempDirectory "test" "out." $ \tmpdir -> do testFiles <- glob "test/*.test" goldenTests <- mapM (getTest tmpdir) testFiles defaultMain $ testGroup "Tests" [ testGroup "Golden tests" goldenTests , testGroup "Unit tests" unitTests ] unitTests :: [TestTree] unitTests = [ testCase "compile failure" $ do (res :: Either String (Template T.Text)) <- compileTemplate "" "$if(x$and$endif$" res @?= Left "(line 1, column 6):\nunexpected \"$\"\nexpecting \".\", \"/\" or \")\"" , testCase "compile failure (keyword as variable)" $ do (res :: Either String (Template T.Text)) <- compileTemplate "foobar.txt" "$sep$" res @?= Left "\"foobar.txt\" (line 1, column 5):\nunexpected \"$\"\nexpecting letter or digit or \"()\"" , testCase "compile failure (unknown pipe)" $ do (res :: Either String (Template T.Text)) <- compileTemplate "foobar.txt" "$foo/nope$" res @?= Left "\"foobar.txt\" (line 1, column 10):\nunexpected \"$\"\nexpecting letter, letter or digit or \"()\"\nUnknown pipe nope" , testCase "compile failure (missing parameter for pipe)" $ do (res :: Either String (Template T.Text)) <- compileTemplate "foobar.txt" "$foo/left$" res @?= Left "\"foobar.txt\" (line 1, column 10):\nunexpected \"$\"\nexpecting letter, integer parameter for pipe, letter or digit or \"()\"" , testCase "compile failure (unexpected parameter for pipe)" $ do (res :: Either String (Template T.Text)) <- compileTemplate "foobar.txt" "$foo/left a$" res @?= Left "\"foobar.txt\" (line 1, column 11):\nunexpected \"a\"\nexpecting integer parameter for pipe" , testCase "compile failure (error in partial)" $ do (res :: Either String (Template T.Text)) <- compileTemplate "test/foobar.txt" "$bad()$" res @?= Left "\"test/bad.txt\" (line 2, column 7):\nunexpected \"s\"\nexpecting \"$\"" , testCase "comment with no newline" $ do (res :: Either String (Template T.Text)) <- compileTemplate "foo" "$-- hi" res @?= Right (mempty :: Template T.Text) , testCase "reflow" $ do (templ :: Either String (Template T.Text)) <- compileTemplate "foo" "not breakable and$~$ this is breakable\nok? $foo$$~$" let res :: T.Text res = case templ of Right t -> render (Just 10) (renderTemplate t (object ["foo" .= ("42" :: T.Text)])) Left e -> T.pack e res @?= "not breakable and\nthis is\nbreakable\nok? 42" , testCase "nowrap pipe" $ do (templ :: Either String (Template T.Text)) <- compileTemplate "foo" "$foo/nowrap$\n$foo$" let res :: T.Text res = case templ of Right t -> render (Just 10) (renderTemplate t (Context $ M.insert "foo" (SimpleVal $ DL.hsep ["hello", "this", "is", "a", "test", "of", "the", "wrapping"] :: 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" ] {- The test "golden" files are structured as follows: { "foo": ["bar", "baz"] } . A template with $foo$. . A template with bar, baz. -} diff :: FilePath -> FilePath -> [String] diff ref new = ["diff", "-u", "--minimal", ref, new] getTest :: FilePath -> FilePath -> IO TestTree getTest tmpdir fp = do let actual = tmpdir takeFileName fp return $ goldenVsFileDiff fp diff fp actual $ do inp <- T.readFile fp let (j, template', _expected) = case T.splitOn "\n.\n" inp of [x,y,z] -> (x,y,z) _ -> error $ "Error parsing " ++ fp let j' = j <> "\n" let template = template' <> "\n" let templatePath = replaceExtension fp ".txt" let (context :: Value) = fromMaybe Null $ decode' . BL.fromStrict . T.encodeUtf8 $ j' res <- applyTemplate templatePath template context case res of Left e -> error e Right x -> T.writeFile actual $ j' <> ".\n" <> template <> ".\n" <> render Nothing x doctemplates-0.11/test/values.test0000644000000000000000000000030207346545000015462 0ustar0000000000000000{ "foo": 1, "bar": null, "baz": ["a", "b"], "bim": { "zub": "sim" }, "sup": [ { "biz": "qux" } , { "sax": 2 } ] } . $foo$ $bar$ $baz$ $bim$ $sup$ . 1 ab true truetrue