doctemplates 0.6.1 → 0.7
raw patch · 18 files changed
+1017/−362 lines, 18 filesdep +HsYAMLdep +text-conversionsdep ~doclayoutPVP ok
version bump matches the API change (PVP)
Dependencies added: HsYAML, text-conversions
Dependency ranges changed: doclayout
API changes (from Hackage documentation)
- Text.DocTemplates: breakingSpace :: TemplateTarget a => a
- Text.DocTemplates: class Monoid a => TemplateTarget a
- Text.DocTemplates: fromText :: TemplateTarget a => Text -> a
- Text.DocTemplates: indent :: TemplateTarget a => Int -> a -> a
- Text.DocTemplates: isEmpty :: TemplateTarget a => a -> Bool
- Text.DocTemplates: removeFinalNewline :: TemplateTarget a => a -> a
- Text.DocTemplates: toText :: TemplateTarget a => a -> Text
- Text.DocTemplates.Internal: BreakingSpace :: Template
- Text.DocTemplates.Internal: [unVariable] :: Variable -> [Text]
- Text.DocTemplates.Internal: breakingSpace :: TemplateTarget a => a
- Text.DocTemplates.Internal: class Monoid a => TemplateTarget a
- Text.DocTemplates.Internal: fromText :: TemplateTarget a => Text -> a
- Text.DocTemplates.Internal: indent :: TemplateTarget a => Int -> a -> a
- Text.DocTemplates.Internal: instance (Text.DocLayout.HasChars a, Data.String.IsString a, GHC.Classes.Eq a) => Text.DocTemplates.Internal.TemplateTarget (Text.DocLayout.Doc a)
- Text.DocTemplates.Internal: instance (Text.DocTemplates.Internal.TemplateTarget a, Text.DocLayout.HasChars a) => Text.DocTemplates.Internal.ToContext (Text.DocLayout.Doc a) a
- Text.DocTemplates.Internal: instance Data.Data.Data Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Base.Monoid Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Base.Semigroup Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Classes.Eq Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Classes.Ord Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Generics.Generic Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Read.Read Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance GHC.Show.Show Text.DocTemplates.Internal.Template
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget Data.Text.Internal.Lazy.Text
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget Data.Text.Internal.Text
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget GHC.Base.String
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Data.Aeson.Types.FromJSON.FromJSON (Text.DocTemplates.Internal.Context a)
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Data.Aeson.Types.FromJSON.FromJSON (Text.DocTemplates.Internal.Val a)
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Text.DocTemplates.Internal.ToContext a (Text.DocTemplates.Internal.Context a)
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Text.DocTemplates.Internal.ToContext a (Text.DocTemplates.Internal.Val a)
- Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Text.DocTemplates.Internal.ToContext a Data.Aeson.Types.Internal.Value
- Text.DocTemplates.Internal: isEmpty :: TemplateTarget a => a -> Bool
- Text.DocTemplates.Internal: newtype Variable
- Text.DocTemplates.Internal: removeFinalNewline :: TemplateTarget a => a -> a
- Text.DocTemplates.Internal: toText :: TemplateTarget a => a -> Text
+ Text.DocTemplates: AfterBreak :: Text -> Doc a
+ Text.DocTemplates: BeforeNonBlank :: Doc a -> Doc a
+ Text.DocTemplates: BlankLines :: Int -> Doc a
+ Text.DocTemplates: Block :: Int -> [a] -> Doc a
+ Text.DocTemplates: BreakingSpace :: Doc a
+ Text.DocTemplates: CarriageReturn :: Doc a
+ Text.DocTemplates: Concat :: Doc a -> Doc a -> Doc a
+ Text.DocTemplates: Empty :: Doc a
+ Text.DocTemplates: Flush :: Doc a -> Doc a
+ Text.DocTemplates: NewLine :: Doc a
+ Text.DocTemplates: Prefixed :: Text -> Doc a -> Doc a
+ Text.DocTemplates: Text :: Int -> a -> Doc a
+ Text.DocTemplates: VFill :: Int -> a -> Doc a
+ Text.DocTemplates: data Doc a
+ Text.DocTemplates: type TemplateTarget a = (Monoid a, IsString a, HasChars a, ToText a, FromText a)
+ Text.DocTemplates.Internal: Block :: Alignment -> Int -> Border -> Filter
+ Text.DocTemplates.Internal: Border :: Text -> Text -> Border
+ Text.DocTemplates.Internal: Centered :: Alignment
+ Text.DocTemplates.Internal: LeftAligned :: Alignment
+ Text.DocTemplates.Internal: Reverse :: Filter
+ Text.DocTemplates.Internal: RightAligned :: Alignment
+ Text.DocTemplates.Internal: ToAlpha :: Filter
+ Text.DocTemplates.Internal: ToLength :: Filter
+ Text.DocTemplates.Internal: ToLowercase :: Filter
+ Text.DocTemplates.Internal: ToPairs :: Filter
+ Text.DocTemplates.Internal: ToRoman :: Filter
+ Text.DocTemplates.Internal: ToUppercase :: Filter
+ Text.DocTemplates.Internal: [borderLeft] :: Border -> Text
+ Text.DocTemplates.Internal: [borderRight] :: Border -> Text
+ Text.DocTemplates.Internal: [varFilters] :: Variable -> [Filter]
+ Text.DocTemplates.Internal: [varParts] :: Variable -> [Text]
+ Text.DocTemplates.Internal: data Alignment
+ Text.DocTemplates.Internal: data Border
+ Text.DocTemplates.Internal: data Filter
+ Text.DocTemplates.Internal: data Variable
+ Text.DocTemplates.Internal: instance (Data.String.IsString a, Text.DocTemplates.Internal.TemplateTarget a) => Data.Aeson.Types.FromJSON.FromJSON (Text.DocTemplates.Internal.Context a)
+ Text.DocTemplates.Internal: instance (Data.String.IsString a, Text.DocTemplates.Internal.TemplateTarget a) => Data.Aeson.Types.FromJSON.FromJSON (Text.DocTemplates.Internal.Val a)
+ Text.DocTemplates.Internal: instance (Data.String.IsString a, Text.DocTemplates.Internal.TemplateTarget a) => Data.YAML.FromYAML (Text.DocTemplates.Internal.Context a)
+ Text.DocTemplates.Internal: instance (Data.String.IsString a, Text.DocTemplates.Internal.TemplateTarget a) => Text.DocTemplates.Internal.ToContext a Data.Aeson.Types.Internal.Value
+ Text.DocTemplates.Internal: instance Data.Data.Data Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance Data.Data.Data Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance Data.Data.Data Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance Data.Data.Data a => Data.Data.Data (Text.DocTemplates.Internal.Context a)
+ Text.DocTemplates.Internal: instance Data.Data.Data a => Data.Data.Data (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance Data.Data.Data a => Data.Data.Data (Text.DocTemplates.Internal.Val a)
+ Text.DocTemplates.Internal: instance Data.Foldable.Foldable Text.DocTemplates.Internal.Template
+ Text.DocTemplates.Internal: instance Data.Traversable.Traversable Text.DocTemplates.Internal.Template
+ Text.DocTemplates.Internal: instance GHC.Base.Functor Text.DocTemplates.Internal.Template
+ Text.DocTemplates.Internal: instance GHC.Base.Semigroup a => GHC.Base.Monoid (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Base.Semigroup a => GHC.Base.Semigroup (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Classes.Eq Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance GHC.Classes.Eq Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance GHC.Classes.Eq Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance GHC.Classes.Eq a => GHC.Classes.Eq (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Classes.Ord Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance GHC.Classes.Ord Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance GHC.Classes.Ord Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance GHC.Classes.Ord a => GHC.Classes.Ord (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Generics.Generic (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Generics.Generic Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance GHC.Generics.Generic Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance GHC.Generics.Generic Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance GHC.Read.Read Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance GHC.Read.Read Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance GHC.Read.Read Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance GHC.Read.Read a => GHC.Read.Read (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance GHC.Show.Show Text.DocTemplates.Internal.Alignment
+ Text.DocTemplates.Internal: instance GHC.Show.Show Text.DocTemplates.Internal.Border
+ Text.DocTemplates.Internal: instance GHC.Show.Show Text.DocTemplates.Internal.Filter
+ Text.DocTemplates.Internal: instance GHC.Show.Show a => GHC.Show.Show (Text.DocTemplates.Internal.Template a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Data.YAML.FromYAML (Text.DocTemplates.Internal.Val a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Data.YAML.ToYAML (Text.DocTemplates.Internal.Context a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Data.YAML.ToYAML (Text.DocTemplates.Internal.Val a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.TemplateTarget a => Text.DocTemplates.Internal.FromContext a (Text.DocLayout.Doc a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.ToContext GHC.Base.String (Text.DocLayout.Doc GHC.Base.String)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.ToContext a (Text.DocTemplates.Internal.Context a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.ToContext a (Text.DocTemplates.Internal.Val a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.ToContext a a => Text.DocTemplates.Internal.ToContext a (Text.DocLayout.Doc a)
+ Text.DocTemplates.Internal: instance Text.DocTemplates.Internal.ToContext a b => Text.DocTemplates.Internal.ToContext a (Data.Map.Internal.Map Data.Text.Internal.Text b)
+ Text.DocTemplates.Internal: type TemplateTarget a = (Monoid a, IsString a, HasChars a, ToText a, FromText a)
- Text.DocTemplates: SimpleVal :: a -> Val a
+ Text.DocTemplates: SimpleVal :: Doc a -> Val a
- Text.DocTemplates: applyTemplate :: (TemplateMonad m, TemplateTarget a, ToContext a b) => FilePath -> Text -> b -> m (Either String a)
+ Text.DocTemplates: applyTemplate :: (TemplateMonad m, TemplateTarget a, ToContext a b) => FilePath -> Text -> b -> m (Either String (Doc a))
- Text.DocTemplates: compileTemplate :: TemplateMonad m => FilePath -> Text -> m (Either String Template)
+ Text.DocTemplates: compileTemplate :: (TemplateMonad m, TemplateTarget a) => FilePath -> Text -> m (Either String (Template a))
- Text.DocTemplates: compileTemplateFile :: FilePath -> IO (Either String Template)
+ Text.DocTemplates: compileTemplateFile :: TemplateTarget a => FilePath -> IO (Either String (Template a))
- Text.DocTemplates: data Template
+ Text.DocTemplates: data Template a
- Text.DocTemplates: renderTemplate :: (TemplateTarget a, ToContext a b) => Template -> b -> a
+ Text.DocTemplates: renderTemplate :: (TemplateTarget a, ToContext a b) => Template a -> b -> Doc a
- Text.DocTemplates.Internal: Concat :: Template -> Template -> Template
+ Text.DocTemplates.Internal: Concat :: Template a -> Template a -> Template a
- Text.DocTemplates.Internal: Conditional :: Variable -> Template -> Template -> Template
+ Text.DocTemplates.Internal: Conditional :: Variable -> Template a -> Template a -> Template a
- Text.DocTemplates.Internal: Empty :: Template
+ Text.DocTemplates.Internal: Empty :: Template a
- Text.DocTemplates.Internal: Interpolate :: Variable -> Template
+ Text.DocTemplates.Internal: Interpolate :: Variable -> Template a
- Text.DocTemplates.Internal: Iterate :: Variable -> Template -> Template -> Template
+ Text.DocTemplates.Internal: Iterate :: Variable -> Template a -> Template a -> Template a
- Text.DocTemplates.Internal: Literal :: Text -> Template
+ Text.DocTemplates.Internal: Literal :: Doc a -> Template a
- Text.DocTemplates.Internal: Nested :: Int -> Template -> Template
+ Text.DocTemplates.Internal: Nested :: Template a -> Template a
- Text.DocTemplates.Internal: Partial :: Template -> Template
+ Text.DocTemplates.Internal: Partial :: Template a -> Template a
- Text.DocTemplates.Internal: SimpleVal :: a -> Val a
+ Text.DocTemplates.Internal: SimpleVal :: Doc a -> Val a
- Text.DocTemplates.Internal: Variable :: [Text] -> Variable
+ Text.DocTemplates.Internal: Variable :: [Text] -> [Filter] -> Variable
- Text.DocTemplates.Internal: data Template
+ Text.DocTemplates.Internal: data Template a
- Text.DocTemplates.Internal: renderTemplate :: (TemplateTarget a, ToContext a b) => Template -> b -> a
+ Text.DocTemplates.Internal: renderTemplate :: (TemplateTarget a, ToContext a b) => Template a -> b -> Doc a
- Text.DocTemplates.Parser: compileTemplate :: TemplateMonad m => FilePath -> Text -> m (Either String Template)
+ Text.DocTemplates.Parser: compileTemplate :: (TemplateMonad m, TemplateTarget a) => FilePath -> Text -> m (Either String (Template a))
Files
- README.md +153/−41
- bench/bench.hs +4/−2
- changelog.md +9/−0
- doctemplates.cabal +11/−9
- src/Text/DocTemplates.hs +136/−39
- src/Text/DocTemplates/Internal.hs +262/−130
- src/Text/DocTemplates/Parser.hs +212/−114
- test/boilerplate.txt +1/−0
- test/enum.txt +2/−0
- test/filters.test +61/−0
- test/forloop.test +5/−5
- test/keyval.txt +1/−0
- test/loop-in-partial.test +1/−0
- test/nest.test +67/−0
- test/pad.test +53/−0
- test/partial_foo.txt +4/−0
- test/partials.test +6/−0
- test/test.hs +29/−22
README.md view
@@ -1,22 +1,31 @@ # doctemplates -This is the text templating system used by pandoc. Its basic-function is to fill holes in a template in a "Context" that-provides values for variables. Control structures are provided-to test that 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 also supported.+[](https://github.com/jgm/doctemplates/actions) -Templates may be rendered to lazy or strict `Text`, `String`,-or a doclayout `Doc`. (Using a `Doc` allows rendered documents-to wrap flexibly on breaking spaces.) A Context can be-constructed manually or an aeson `Value` may be used. -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.+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. Filters+can be used to transform the values of variables or partials.+The provided filters 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@@ -25,6 +34,7 @@ import qualified Data.Text.IO as T import Data.Aeson import Text.DocTemplates+import Text.DocLayout (render) data Employee = Employee { firstName :: String , lastName :: String@@ -42,7 +52,7 @@ res <- compileTemplate "mytemplate.txt" template case res of Left e -> error e- Right t -> T.putStrLn $ renderTemplate t $ object+ Right t -> T.putStrLn $ render Nothing $ renderTemplate t $ object ["employee" .= [ Employee "John" "Doe" Nothing , Employee "Omar" "Smith" (Just 30000)@@ -109,9 +119,6 @@ - If the value is a JSON boolean, it will be rendered as `true` if true, and as the empty string if false. -The value of a variable that occurs by itself on a line-will be indented to the same level as the opening delimiter of-the variable. ## Conditionals @@ -180,12 +187,14 @@ 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. If the value of the-associated variable is not an array, a single iteration will be-performed on its value.+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 the value of the associated variable is not an array or+ a map, a single iteration will be performed on its value.+ Examples: ```@@ -250,8 +259,8 @@ ``` 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+values, applying the partial `bibentry()` to each one. So the+second example above is equivalent to ``` ${ for(articles) }@@ -284,39 +293,142 @@ 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 `+reflow` keyword (ended-with `-reflow`).+in part of the template by using the `~` keyword (ended+with another `~`). ```-${ +reflow }This long line may break if the document is rendered-with a short line length.${ -reflow }+$~$This long line may break if the document is rendered+with a short line length.$~$ ``` -The `+` keyword has no effect when rendering to `Text`+The `~` keyword has no effect when rendering to `Text` or `String`. -## Nesting+## Filters -As noted above, the value of a variable that occurs by itself on-a line will be indented to the same level as the opening-delimiter of the variable.+A filter transforms the value of a variable. Filters are+specified using a slash (`/`) between the variable name and+the filter name. They may go anywhere a variable can go.+Example: -In addition, any part of a template can be marked explicitly for-indented rendering, using the `+nest` keyword (o start nesting at-the column where it appears) and `-nest` to stop nesting.+```+$for(name)$+$name/uppercase$+$endfor$ -Example:+$for(metadata/pairs)$+- $it.key$: $it.value$+$endfor$+``` +Filters may be chained:+ ```-$for(article)$-- $+nest$$article.author$, "$article.title$," in $article.book$- ($article.year$).$-nest$+$for(employees/pairs)$+$it.key/alpha/uppercase$. $it.name$ $endfor$ ```++Some filters take parameters:++```+|----------------------|------------|+$for(employee)$+$it.name.first/uppercase/left 20 "| "$$it.name.salary/right 10 " | " " |"$+$endfor$+|----------------------|------------|+```++Currently the following filters 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.++- `uppercase`: Converts a textual value to uppercase,+ and has no effect on other values.++- `lowercase`: Converts a textual value to lowercase,+ and has no effect on other values.++- `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.++- `alpha`: Converts a textual value that can be+ read as an integer into a lowercase alphabetic+ character `a..z` (mod 26), and has no effect on+ other values. This can be used to get lettered+ enumeration from array indices. To get uppercase+ letters, chain with `uppercase`.++- `roman`: Converts a textual value that can be+ read as an integer into a lowercase roman numerial,+ and has no effect on other values. 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.
bench/bench.hs view
@@ -8,17 +8,19 @@ 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 (runIdentity . applyTemplate "bigtext" bigtext+ nf (fmap (render Nothing)+ . runIdentity . applyTemplate "bigtext" bigtext :: Value -> Either String Text) val , bench "renderTemplate" $- nf (renderTemplate bigtextTemplate :: Value -> Text)+ nf (render Nothing . renderTemplate bigtextTemplate :: Value -> Text) val ]
changelog.md view
@@ -1,5 +1,14 @@ # doctemplates +## 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.
doctemplates.cabal view
@@ -1,19 +1,18 @@ name: doctemplates-version: 0.6.1+version: 0.7 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, and partials. Templates- may be rendered to a variety of formats, including- doclayout Docs, and variable values may come from- a variety of different sources, including aeson- Values.+ tests for non-blank values, filters, 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 John MacFarlane+copyright: 2016-19 John MacFarlane category: Text build-type: Simple -- extra-source-files:@@ -31,9 +30,11 @@ Text.DocTemplates.Internal build-depends: base >= 4.9 && < 5, safe,+ text-conversions, aeson,+ HsYAML, text,- doclayout >= 0.1 && < 0.2,+ doclayout >= 0.2 && < 0.3, containers, vector, filepath,@@ -52,7 +53,7 @@ main-is: test.hs build-depends: base, doctemplates,- doclayout >= 0.1 && < 0.2,+ doclayout >= 0.2 && < 0.3, aeson, Glob, tasty,@@ -70,6 +71,7 @@ Main-Is: bench.hs Hs-Source-Dirs: bench Build-Depends: doctemplates,+ doclayout >= 0.2 && < 0.3, base >= 4.8 && < 5, criterion >= 1.0 && < 1.6, filepath,
src/Text/DocTemplates.hs view
@@ -8,15 +8,22 @@ Portability : portable This is the text templating system used by pandoc. Its basic function is-to fill holes in a template in a 'Context' that provides values for-variables. Control structures are provided to test that a variable has a+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 also supported.+is, subtemplates defined in different files—are supported. Filters can+be used to transform the values of variables or partials. The provided+filters make it possible to do list enumeration and tabular layout in+templates. -Templates may be rendered to lazy or strict 'Text', 'String', or a-doclayout 'Doc'. (Using a 'Doc' allows rendered documents to wrap-flexibly on breaking spaces.) A 'Context' can be constructed manually or-an aeson 'Value' may be used.+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@@ -29,6 +36,7 @@ > import qualified Data.Text.IO as T > import Data.Aeson > import Text.DocTemplates+> import Text.DocLayout (render) > > data Employee = Employee { firstName :: String > , lastName :: String@@ -46,7 +54,7 @@ > res <- compileTemplate "mytemplate.txt" template > case res of > Left e -> error e-> Right t -> T.putStrLn $ renderTemplate t $ object+> Right t -> T.putStrLn $ render Nothing $ renderTemplate t $ object > ["employee" .= > [ Employee "John" "Doe" Nothing > , Employee "Omar" "Smith" (Just 30000)@@ -86,8 +94,8 @@ > ${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 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.@@ -100,7 +108,7 @@ - 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+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@@ -108,9 +116,6 @@ - If the value is a JSON boolean, it will be rendered as @true@ if true, and as the empty string if false. -The value of a variable that occurs by itself on a line will be indented-to the same level as the opening delimiter of the variable.- == Conditionals A conditional begins with @if(variable)@ (enclosed in matched@@ -170,12 +175,14 @@ == 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. If the-value of the associated variable is not an array, a single iteration-will be performed on its value.+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 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$@@ -254,36 +261,123 @@ 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-@+reflow@ keyword (ended with @-reflow@).+they can be made breakable in part of the template by using the @~@+keyword (ended with another @~@). -> ${ +reflow }This long line may break if the document is rendered-> with a short line length.${ -reflow }+> $~$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@.+The @~@ keyword has no effect when rendering to @Text@ or @String@. -== Nesting+== Filters -As noted above, the value of a variable that occurs by itself on a line-will be indented to the same level as the opening delimiter of the-variable.+A filter transforms the value of a variable. Filters are specified using+a slash (@\/@) between the variable name and the filter name. They may+go anywhere a variable can go. Example: -In addition, any part of a template can be marked explicitly for-indented rendering, using the @+nest@ keyword (o start nesting at the-column where it appears) and @-nest@ to stop nesting.+> $for(name)$+> $name/uppercase$+> $endfor$+>+> $for(metadata/pairs)$+> - $it.key$: $it.value$+> $endfor$ -Example:+Filters may be chained: -> $for(article)$-> - $+nest$$article.author$, "$article.title$," in $article.book$-> ($article.year$).$-nest$+> $for(employees/pairs)$+> $it.key/alpha/uppercase$. $it.name$ > $endfor$ +Some filters take parameters:++> |----------------------|------------|+> $for(employee)$+> $it.name.first/uppercase/left 20 "| "$$it.name.salary/right 10 " | " " |"$+> $endfor$+> |----------------------|------------|++Currently the following filters 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.++- @uppercase@: Converts a textual value to uppercase, and has no+ effect on other values.++- @lowercase@: Converts a textual value to lowercase, and has no+ effect on other values.++- @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.++- @alpha@: Converts a textual value that can be read as an integer+ into a lowercase alphabetic character @a..z@ (mod 26), and has no+ effect on other values. This can be used to get lettered enumeration+ from array indices. To get uppercase letters, chain with+ @uppercase@.++- @roman@: Converts a textual value that can be read as an integer+ into a lowercase roman numerial, and has no effect on other values.+ 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@@ -291,25 +385,28 @@ , compileTemplateFile , applyTemplate , TemplateMonad(..)- , TemplateTarget(..)+ , 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(..),+ 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 :: FilePath -> IO (Either String Template)+compileTemplateFile :: TemplateTarget a+ => FilePath -> IO (Either String (Template a)) compileTemplateFile templPath = do templateText <- TIO.readFile templPath compileTemplate templPath templateText@@ -320,7 +417,7 @@ -- 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 a)+ => FilePath -> Text -> b -> m (Either String (Doc a)) applyTemplate fp t val = do res <- compileTemplate fp t case res of
src/Text/DocTemplates/Internal.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeSynonymInstances #-}@@ -29,17 +30,24 @@ , Val(..) , ToContext(..) , FromContext(..)- , TemplateTarget(..)+ , TemplateTarget , Template(..) , Variable(..)+ , Filter(..)+ , Alignment(..)+ , Border(..) ) where -import Safe (lastMay, initDef)+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 qualified Data.Text.Read as T import qualified Data.Text as T-import qualified Data.Text.Lazy as TL 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)@@ -50,114 +58,87 @@ import qualified Data.HashMap.Strict as H import qualified Data.Vector as V import Data.Scientific (floatingOrInteger)-import Data.List (intersperse, intercalate)+import Data.List (intersperse) #if MIN_VERSION_base(4,11,0) #else import Data.Semigroup #endif -- | A template.-data Template =+data Template a = Interpolate Variable- | Conditional Variable Template Template- | Iterate Variable Template Template- | Nested Int Template- | Partial Template- | Literal Text- | Concat Template Template- | BreakingSpace+ | Conditional Variable (Template a) (Template a)+ | Iterate Variable (Template a) (Template a)+ | Nested (Template a)+ | Partial (Template a)+ | Literal (Doc a)+ | Concat (Template a) (Template a) | Empty- deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)+ deriving (Show, Read, Data, Typeable, Generic, Eq, Ord,+ Foldable, Traversable, Functor) -instance Semigroup Template where+instance Semigroup a => Semigroup (Template a) where x <> Empty = x Empty <> x = x x <> y = Concat x y -instance Monoid Template where+instance Semigroup a => Monoid (Template a) where mappend = (<>) mempty = Empty +data Filter =+ ToPairs+ | ToUppercase+ | ToLowercase+ | ToLength+ | Reverse+ | ToAlpha+ | ToRoman+ | 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@).-newtype Variable = Variable { unVariable :: [Text] }+data Variable =+ Variable+ { varParts :: [Text]+ , varFilters :: [Filter]+ } deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) instance Semigroup Variable where- Variable xs <> Variable ys = Variable (xs <> ys)+ Variable xs fs <> Variable ys gs = Variable (xs <> ys) (fs <> gs) instance Monoid Variable where- mempty = Variable []+ mempty = Variable mempty mempty mappend = (<>) --- | A type to which templates can be rendered.-class Monoid a => TemplateTarget a where- fromText :: Text -> a- toText :: a -> Text- removeFinalNewline :: a -> a- isEmpty :: a -> Bool- indent :: Int -> a -> a- breakingSpace :: a--instance TemplateTarget Text where- fromText = id- toText = id- removeFinalNewline t =- case T.unsnoc t of- Just (t', '\n') -> t'- _ -> t- isEmpty = T.null- indent 0 = id- indent ind = T.intercalate ("\n" <> T.replicate ind " ") . T.lines- breakingSpace = " "--instance TemplateTarget TL.Text where- fromText = TL.fromStrict- toText = TL.toStrict- removeFinalNewline t =- case TL.unsnoc t of- Just (t', '\n') -> t'- _ -> t- isEmpty = TL.null- indent 0 = id- indent ind = TL.intercalate ("\n" <> TL.replicate (fromIntegral ind) " ")- . TL.lines- breakingSpace = " "--instance TemplateTarget String where- fromText = T.unpack- toText = T.pack- removeFinalNewline t =- case lastMay t of- Just '\n' -> initDef t t- _ -> t- isEmpty = null- indent 0 = id- indent ind = intercalate ("\n" <> replicate ind ' ') . lines- breakingSpace = " "--instance (DL.HasChars a, IsString a, Eq a)- => TemplateTarget (DL.Doc a) where- fromText = DL.text . T.unpack- toText = T.pack . DL.foldrChar (:) [] . DL.render Nothing- removeFinalNewline = DL.chomp- indent = DL.nest- isEmpty (DL.Empty) = True- isEmpty (DL.Text 0 _) = True- isEmpty (DL.Concat x y) = isEmpty x && isEmpty y- isEmpty _ = False- breakingSpace = DL.space+type TemplateTarget a =+ (Monoid a, IsString 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)+ deriving (Show, Semigroup, Monoid, Traversable, Foldable, Functor,+ Data, Typeable) -- | A variable value. data Val a =- SimpleVal a+ SimpleVal (Doc a) | ListVal [Val a] | MapVal (Context a) | NullVal- deriving (Show, Traversable, Foldable, Functor)+ deriving (Show, Traversable, Foldable, Functor, Data, Typeable) -- | The 'ToContext' class provides automatic conversion to -- a 'Context' or 'Val'.@@ -168,26 +149,40 @@ _ -> mempty toVal :: b -> Val a -instance TemplateTarget a => ToContext a (Context a) where+instance ToContext a (Context a) where toContext = id toVal = MapVal -instance TemplateTarget a => ToContext a (Val a) where+instance ToContext a (Val a) where toVal = id instance TemplateTarget a => ToContext a a where- toVal = SimpleVal+ toVal = SimpleVal . DL.literal +instance ToContext a a => ToContext a (Doc a) where+ toVal = SimpleVal+ -- This is needed because otherwise the compiler tries to -- match on ToContext a [b], with a = b = Char, even though -- we don't have ToContext Char Char. I don't understand why. instance {-# OVERLAPS #-} ToContext String String where- toVal t = SimpleVal t+ toVal = SimpleVal . DL.literal +instance {-# OVERLAPS #-} ToContext String (Doc String) where+ toVal = SimpleVal+ instance ToContext a b => ToContext a [b] where toVal = ListVal . map toVal -instance TemplateTarget a => ToContext a Value where+instance ToContext a b => ToContext a (M.Map Text b) where+ toVal = MapVal . toContext+ toContext = Context . M.map toVal++instance TemplateTarget a => ToContext a Bool where+ toVal True = SimpleVal "true"+ toVal False = NullVal++instance (IsString a, TemplateTarget a) => ToContext a Value where toContext x = case fromJSON x of Success y -> y Error _ -> mempty@@ -195,13 +190,6 @@ Success y -> y Error _ -> NullVal -instance TemplateTarget a => ToContext a Bool where- toVal True = SimpleVal $ fromText "true"- toVal False = NullVal--instance (TemplateTarget a, DL.HasChars a) => ToContext (DL.Doc a) a where- toVal t = SimpleVal $ DL.Text (DL.realLength t) t- -- | The 'FromContext' class provides functions for extracting -- values from 'Val' and 'Context'. class FromContext a b where@@ -212,42 +200,68 @@ instance TemplateTarget a => FromContext a (Val a) where fromVal = Just -instance TemplateTarget a => FromContext a a where+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+ -- This is needed because otherwise the compiler tries to -- match on FromContext a [b], with a = b = Char, even though -- we don't have FromContext Char Char. I don't understand why. instance {-# OVERLAPS #-} FromContext String String where- fromVal (SimpleVal x) = Just x+ 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 (Context a) where- parseJSON v = do- val <- parseJSON v- case val of- MapVal o -> return o- _ -> fail "Not a MapVal"--instance TemplateTarget a => FromJSON (Val a) where+instance (IsString a, TemplateTarget a) => FromJSON (Val a) where parseJSON v = case v of Array vec -> ListVal <$> mapM parseJSON (V.toList vec)- String t -> return $ SimpleVal $ fromText t- Number n -> return $ SimpleVal $ fromText . fromString $+ 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 True -> return $ SimpleVal $ fromText "true"+ Bool True -> return $ SimpleVal "true" Object o -> MapVal . Context . M.fromList . H.toList <$> mapM parseJSON o _ -> return NullVal +instance (IsString a, 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 => FromYAML (Val a) where+ parseYAML v =+ case v of+ Mapping _ _ m -> MapVal . Context . M.fromList <$>+ mapM (\(key, val) -> do+ val' <- parseYAML val+ key' <- parseYAML key+ return (key', val')) (M.toList m)+ Sequence _ _ xs -> ListVal <$> mapM parseYAML xs+ Scalar _ (SStr t) -> return $ SimpleVal $ fromString . fromText $ t+ Scalar _ (SFloat n) -> return $ SimpleVal $ fromString . show $ n+ Scalar _ (SInt n) -> return $ SimpleVal $ fromString . show $ n+ Scalar _ (SBool True) -> return $ SimpleVal "true"+ _ -> return NullVal++instance (IsString a, 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 @@ -255,9 +269,111 @@ toJSON NullVal = Null toJSON (MapVal m) = toJSON m toJSON (ListVal xs) = toJSON xs- toJSON (SimpleVal t) = toJSON $ toText t+ toJSON (SimpleVal d) = toJSON $ toText $ DL.render Nothing d -multiLookup :: [Text] -> Val a -> Val a+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++applyFilter :: TemplateTarget a => Filter -> Val a -> Val a+applyFilter 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+ NullVal -> 0+applyFilter ToUppercase val =+ case val of+ SimpleVal d -> SimpleVal . runIdentity $+ traverse (pure . fromText . T.toUpper . toText) d+ _ -> val+applyFilter ToLowercase val =+ case val of+ SimpleVal d -> SimpleVal . runIdentity $+ traverse (pure . fromText . T.toLower . toText) d+ _ -> val+applyFilter 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) ]+applyFilter Reverse val =+ case val of+ SimpleVal d -> SimpleVal . runIdentity $+ traverse (pure . fromText . T.reverse . toText) d+ ListVal xs -> ListVal (reverse xs)+ _ -> val+applyFilter ToAlpha val =+ case val of+ SimpleVal (DL.Text _ t) ->+ case T.decimal (toText t) of+ Right (y,"") -> SimpleVal $ fromString+ [chr (ord 'a' + (y `mod` 26) - 1)]+ _ -> val+ _ -> val+applyFilter ToRoman val =+ case val of+ SimpleVal (DL.Text _ t) ->+ case T.decimal (toText t) of+ Right (y,"") -> maybe val (SimpleVal . DL.literal . fromText)+ (toRoman y)+ _ -> val+ _ -> val+applyFilter (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++applyFilters :: TemplateTarget a => [Filter] -> Val a -> Val a+applyFilters fs x = foldr applyFilter 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@@ -265,52 +381,68 @@ Just v' -> multiLookup vs v' multiLookup _ _ = NullVal -resolveVariable :: TemplateTarget a => Variable -> Context a -> [a]+resolveVariable :: TemplateTarget a => Variable -> Context a -> [Doc a] resolveVariable v ctx = resolveVariable' v (MapVal ctx) -resolveVariable' :: TemplateTarget a => Variable -> Val a -> [a]+resolveVariable' :: TemplateTarget a => Variable -> Val a -> [Doc a] resolveVariable' v val =- case multiLookup (unVariable v) val of+ case applyFilters (varFilters v) $ multiLookup (varParts v) val of ListVal xs -> concatMap (resolveVariable' mempty) xs- SimpleVal t- | isEmpty t -> []- | otherwise -> [removeFinalNewline t]- MapVal _ -> [fromText "true"]+ SimpleVal d+ | DL.isEmpty d -> []+ | otherwise -> [removeFinalNl d]+ MapVal _ -> ["true"] NullVal -> [] -withVariable :: TemplateTarget a- => Variable -> Context a -> (Context a -> a) -> [a]+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 v ctx f =- case multiLookup (unVariable v) (MapVal ctx) of- NullVal -> mempty- ListVal xs -> map (\iterval -> f $+ case applyFilters (varFilters v) $ multiLookup (varParts v) (MapVal ctx) of+ NullVal -> return mempty+ ListVal xs -> mapM (\iterval -> f $ Context $ M.insert "it" iterval $ unContext ctx) xs- val' -> [f $ Context $ M.insert "it" val' $ unContext ctx]+ val' -> (:[]) <$> f (Context $ M.insert "it" val' $ unContext 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 -> b -> a-renderTemplate t = renderTemp t . toContext+ => 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 -> Context a -> a-renderTemp (Literal t) _ = fromText t-renderTemp BreakingSpace _ = breakingSpace-renderTemp (Interpolate v) ctx = mconcat $ resolveVariable v ctx+ => Template a -> Context a -> RenderState (Doc a)+renderTemp (Literal t) _ = updateColumn $ t+renderTemp (Interpolate v) ctx = updateColumn $ mconcat $ resolveVariable v ctx renderTemp (Conditional v ift elset) ctx = let res = resolveVariable v ctx in case res of [] -> renderTemp elset ctx _ -> renderTemp ift ctx-renderTemp (Iterate v t sep) ctx =- let sep' = renderTemp sep ctx- in mconcat . intersperse sep' $ withVariable v ctx (renderTemp t)-renderTemp (Nested n t) ctx = indent n $ renderTemp t 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 t) ctx = renderTemp t ctx renderTemp (Concat t1 t2) ctx =- mappend (renderTemp t1 ctx) (renderTemp t2 ctx)-renderTemp Empty _ = mempty+ 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
src/Text/DocTemplates/Parser.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-} {- | Module : Text.DocTemplates.Parser Copyright : Copyright (C) 2009-2019 John MacFarlane@@ -21,76 +22,104 @@ 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 Data.List (isPrefixOf) import System.FilePath import Text.DocTemplates.Internal+import qualified Text.DocLayout as DL #if MIN_VERSION_base(4,11,0) #else-import Data.Semigroup ((<>))+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- => FilePath -> Text -> m (Either String Template)+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- , indentLevel = 0- , beginsLine = True- , breakingSpaces = False } templPath template+ 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- , indentLevel :: Int- , beginsLine :: Bool- , breakingSpaces :: Bool }+ PState { templatePath :: FilePath+ , partialNesting :: !Int+ , breakingSpaces :: !Bool+ , firstNonspace :: P.SourcePos+ , nestedCol :: Maybe Int+ , insideDirective :: Bool+ } type Parser = P.ParsecT Text PState -pTemplate :: TemplateMonad m => Parser m Template+pTemplate :: (TemplateMonad m, TemplateTarget a) => Parser m (Template a) pTemplate = do P.skipMany pComment mconcat <$> many- ((pLit <|> pNewline <|> pDirective <|> pEscape) <* P.skipMany pComment)+ ((pLit <|> pNewline <|> pDirective <|>+ pEscape) <* P.skipMany pComment) -pNewline :: Monad m => Parser m Template-pNewline = do- nls <- P.string "\n" <|> P.string "\r" <|> P.string "\r\n"+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- return $- if breakspaces- then BreakingSpace- else Literal $ fromString nls+ pos <- P.getPosition+ P.updateState $ \st -> st{ firstNonspace = pos }+ return $ Literal $+ if breakspaces+ then DL.BreakingSpace+ else fromString $ nls <> sps -pLit :: Monad m => Parser m Template+pLit :: (TemplateTarget a, Monad m) => Parser m (Template a) pLit = do- col <- P.sourceColumn <$> P.getPosition- ind <- indentLevel <$> P.getState- -- eat up indentlevel spaces- P.skipMany $ do- P.getPosition >>= guard . (< (ind - 1)) . P.sourceColumn- P.satisfy (\c -> c == ' ' || c == '\t') cs <- P.many1 (P.satisfy (\c -> c /= '$' && c /= '\n' && c /= '\r'))- P.updateState $ \st -> st{ beginsLine = col == 1 && all (==' ') cs }+ 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 :: String -> Template+toBreakable :: TemplateTarget a => String -> Template a toBreakable [] = Empty toBreakable xs = case break isSpacy xs of ([], []) -> Empty- ([], zs) -> BreakingSpace <> toBreakable (dropWhile isSpacy zs)+ ([], zs) -> Literal DL.BreakingSpace <>+ toBreakable (dropWhile isSpacy zs) (ys, []) -> Literal (fromString ys) (ys, zs) -> Literal (fromString ys) <> toBreakable zs @@ -106,16 +135,14 @@ pos <- P.getPosition P.setPosition $ P.incSourceColumn pos (- n) -pEscape :: Monad m => Parser m Template+pEscape :: (TemplateTarget a, Monad m) => Parser m (Template a) pEscape = Literal "$" <$ P.try (P.string "$$" <* backupSourcePos 1) -pDirective :: TemplateMonad m- => Parser m Template+pDirective :: (TemplateTarget a, TemplateMonad m)+ => Parser m (Template a) pDirective = do- res <- pConditional <|> pForLoop <|> pReflow <|> pNest <|>+ res <- pConditional <|> pForLoop <|> pReflowToggle <|> pNested <|> pInterpolate <|> pBarePartial- col <- P.sourceColumn <$> P.getPosition- P.updateState $ \st -> st{ beginsLine = col == 1 } return res pEnclosed :: Monad m => Parser m a -> Parser m a@@ -134,25 +161,37 @@ P.char ')' return result -pConditional :: TemplateMonad m- => Parser m Template+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- -- if newline after the "if", then a newline after "endif" will be swallowed- multiline <- P.option False (True <$ skipEndline)- ifContents <- pTemplate- elseContents <- P.option mempty (pElse multiline <|> pElseIf)- pEnclosed (P.string "endif")- when multiline $ P.option () skipEndline- return $ Conditional v ifContents elseContents+ 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 :: TemplateMonad m => Bool -> Parser m Template+pElse :: (TemplateTarget a, TemplateMonad m)+ => Bool -> Parser m (Template a) pElse multiline = do pEnclosed (P.string "else") when multiline $ P.option () skipEndline pTemplate -pElseIf :: TemplateMonad m => Parser m Template+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)@@ -161,44 +200,51 @@ return $ Conditional v ifContents elseContents skipEndline :: Monad m => Parser m ()-skipEndline = P.try $- (P.skipMany pSpaceOrTab <* P.char '\n')- <|> (P.skipMany1 pSpaceOrTab <* P.eof)+skipEndline = do+ pEndline+ pos <- P.lookAhead $ do+ P.skipMany (P.char ' ' <|> P.char '\t')+ P.getPosition+ P.updateState $ \st -> st{ firstNonspace = pos } -pNest :: TemplateMonad m => Parser m Template-pNest = do- col <- P.sourceColumn <$> P.getPosition- pEnclosed $ P.string "+nest"- P.updateState $ \st -> st{ indentLevel = col - 1 }- t <- pTemplate- P.optional $ pEnclosed $ P.string "-nest"- return $ Nested (col - 1) t+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 -pReflow :: TemplateMonad m => Parser m Template-pReflow = mempty <$ (pReflowOn <|> pReflowOff)- where- pReflowOn = do- pEnclosed $ P.string "+reflow"- P.modifyState $ \st -> st{ breakingSpaces = True }- pReflowOff = do- pEnclosed $ P.string "-reflow"- P.modifyState $ \st -> st{ breakingSpaces = False }+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 :: TemplateMonad m => Parser m Template+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- multiline <- P.option False $ skipEndline >> return True- contents <- changeToIt v <$> pTemplate- sep <- P.option mempty $- do pEnclosed (P.string "sep")- when multiline $ P.option () skipEndline- changeToIt v <$> pTemplate- pEnclosed (P.string "endfor")- when multiline $ P.option () skipEndline- return $ Iterate v contents sep+ pInside $ do+ multiline <- P.option False $ skipEndline >> return True+ contents <- changeToIt v <$> pTemplate+ sep <- P.option mempty $+ do pEnclosed (P.string "sep")+ when multiline $ P.option () skipEndline+ changeToIt v <$> pTemplate+ pEnclosed (P.string "endfor")+ when multiline $ P.option () skipEndline+ return $ Iterate v contents sep -changeToIt :: Variable -> Template -> Template+changeToIt :: (Semigroup a) => Variable -> Template a -> Template a changeToIt v = go where go (Interpolate w) = Interpolate (reletter v w)@@ -208,17 +254,16 @@ (changeToIt v t1) (changeToIt v t2) go (Concat t1 t2) = changeToIt v t1 <> changeToIt v t2 go (Partial t) = Partial t -- don't reletter inside partial- go (Nested n t) = Nested n (go t)+ go (Nested t) = Nested (go t) go x = x- reletter (Variable vs) (Variable ws) =+ reletter (Variable vs _fs) (Variable ws gs) = if vs `isPrefixOf` ws- then Variable ("it" : drop (length vs) ws)- else Variable ws+ then Variable ("it" : drop (length vs) ws) gs+ else Variable ws gs -pInterpolate :: TemplateMonad m- => Parser m Template+pInterpolate :: (TemplateTarget a, TemplateMonad m)+ => Parser m (Template a) pInterpolate = do- begins <- beginsLine <$> P.getState pos <- P.getPosition -- we don't used pEnclosed here, to get better error messages: (closer, var) <- P.try $ do@@ -228,27 +273,37 @@ P.notFollowedBy (P.char '(') -- bare partial return (cl, v) res <- (P.char ':' *> (pPartialName >>= pPartial (Just var)))- <|> Iterate var (Interpolate (Variable ["it"])) <$> pSep+ <|> Iterate var (Interpolate (Variable ["it"] [])) <$> pSep <|> return (Interpolate var) P.skipMany pSpaceOrTab closer- ends <- P.lookAhead $ P.option False $- True <$ P.try (P.skipMany pSpaceOrTab *> pNewlineOrEof)- let toNested = case P.sourceColumn pos - 1 of- 0 -> id- i -> Nested i- case (begins && ends, res) of- (True, Interpolate v)- -> return $ toNested $ Interpolate v- (True, Iterate v (Interpolate v') s)- -> return $ toNested $ Iterate v (Interpolate v') s- _ -> return res+ 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 = () <$ P.newline <|> P.eof+pNewlineOrEof = () <$ pLineEnding <|> P.eof -pBarePartial :: TemplateMonad m- => Parser m Template+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@@ -259,10 +314,7 @@ res <- pPartial Nothing fp P.skipMany pSpaceOrTab closer- let toNested = case P.sourceColumn pos - 1 of- 0 -> id- i -> Nested i- return $ toNested res+ handleNesting True pos res pPartialName :: TemplateMonad m => Parser m FilePath@@ -271,9 +323,10 @@ P.string "()" return fp -pPartial :: TemplateMonad m- => Maybe Variable -> FilePath -> Parser m Template+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@@ -289,16 +342,24 @@ 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 case mbvar of Just var -> return $ Iterate var t separ Nothing -> return $ Partial t -pSep :: Monad m => Parser m Template+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 (/= ']'))@@ -315,9 +376,7 @@ 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) $ () <$ do- pNewlineOrEof- P.updateState $ \st -> st{ beginsLine = True }+ when (P.sourceColumn pos == 1) $ () <$ pNewlineOrEof pOpenDollar :: Monad m => Parser m (Parser m ()) pOpenDollar =@@ -338,8 +397,47 @@ pVar :: Monad m => Parser m Variable pVar = do first <- pIdentPart <|> pIt- rest <- P.many $ (P.char '.' *> pIdentPart)- return $ Variable (first:rest)+ rest <- P.many (P.char '.' *> pIdentPart)+ filters <- P.many pFilter+ return $ Variable (first:rest) filters++pFilter :: Monad m => Parser m Filter+pFilter = do+ P.char '/'+ filterName <- P.many1 P.letter+ P.notFollowedBy P.letter+ case filterName of+ "uppercase" -> return ToUppercase+ "lowercase" -> return ToLowercase+ "pairs" -> return ToPairs+ "length" -> return ToLength+ "alpha" -> return ToAlpha+ "roman" -> return ToRoman+ "reverse" -> return Reverse+ "left" -> Block LeftAligned <$> pBlockWidth <*> pBlockBorders+ "right" -> Block RightAligned <$> pBlockWidth <*> pBlockBorders+ "center" -> Block Centered <$> pBlockWidth <*> pBlockBorders+ _ -> fail $ "Unknown filter " ++ filterName++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 filter") P.<?>+ "integer parameter for filter"++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")
test/boilerplate.txt view
@@ -1,2 +1,3 @@ BOILERPLATE HERE+
+ test/enum.txt view
@@ -0,0 +1,2 @@+$it.key/alpha/uppercase$. $^$$it.value$+
+ test/filters.test view
@@ -0,0 +1,61 @@+{ "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" ]+}+.+$bar/length$+$baz/length$+$bim.Zub/length$+$bim/length$+$sup/length$++$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()$+.+0+2+3+1+2++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
test/forloop.test view
@@ -1,9 +1,9 @@ { "employee":- [ { "name": { "first": "John", "last": "Doe" } }- , { "name": { "first": "Omar", "last": "Smith" }+ [ { "name": { "first": "John", "last": "Doe" } , "salary": "30000" }- , { "name": { "first": "Sara", "last": "Chen" }+ , { "name": { "first": "Omar", "last": "Smith" } , "salary": "60000" }+ , { "name": { "first": "Sara", "last": "Chen" } } ] } .@@ -12,10 +12,10 @@ $endfor$ -$for(employee)$$employee.salary$$sep$:$endfor$+$for(employee)$$employee.salary$$sep$; $endfor$ . John Doe; Omar Smith; Sara Chen -:30000:60000+30000; 60000;
+ test/keyval.txt view
@@ -0,0 +1,1 @@+
test/loop-in-partial.test view
@@ -1,5 +1,6 @@ {} . $loop1()$+ . (loop)
+ test/nest.test view
@@ -0,0 +1,67 @@+{ "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
+ test/pad.test view
@@ -0,0 +1,53 @@+{ "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 |+|------------|------------|
+ test/partial_foo.txt view
@@ -0,0 +1,4 @@+Hello+$if(foo)$+$foo$+$endif$
test/partials.test view
@@ -17,7 +17,10 @@ $employee:name.tex()[; ]$ +--- $boilerplate()$+---+$partial_foo()$ . (John) Doe (Omar) Smith@@ -29,5 +32,8 @@ \name{John}{Doe}; \name{Omar}{Smith}; \name{Sara}{Chen} +--- BOILERPLATE HERE+---+Hello
test/test.hs view
@@ -29,36 +29,42 @@ unitTests :: [TestTree] unitTests = [ testCase "compile failure" $ do- res <- compileTemplate "" "$if(x$and$endif$"- res @?= Left "(line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")\""+ (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 <- compileTemplate "foobar.txt" "$sep$"- res @?= Left "\"foobar.txt\" (line 1, column 5):\nunexpected \"$\"\nexpecting letter or digit or \"()\""+ (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 filter)" $ 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 filter nope"+ , testCase "compile failure (missing parameter for filter)" $ 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 filter, letter or digit or \"()\""+ , testCase "compile failure (unexpected parameter for filter)" $ 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 filter" , testCase "compile failure (error in partial)" $ do- res <- compileTemplate "test/foobar.txt" "$bad()$"+ (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 <- compileTemplate "foo" "$-- hi"- res @?= Right mempty+ (res :: Either String (Template T.Text)) <-+ compileTemplate "foo" "$-- hi"+ res @?= Right (mempty :: Template T.Text) , testCase "reflow" $ do- templ <- compileTemplate "foo" "not breakable and $+reflow$ this is breakable\nok? $foo$$-reflow$"- 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 "nest" $ do- res <- applyTemplate "foo" "- $+nest$aaa\nbbb$-nest$" (object [])- res @?= Right ("- aaa\n bbb" :: T.Text)- , testCase "implicitly closed nest" $ do- templ <- compileTemplate "foo" "$if(foo)$\n- $+nest$a\nb$endif$"+ (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 @?= "- a\n b"+ res @?= "not breakable and\nthis is\nbreakable\nok? 42" ] {- The test "golden" files are structured as follows:@@ -72,7 +78,7 @@ -} diff :: FilePath -> FilePath -> [String]-diff ref new = ["diff", "-u", ref, new]+diff ref new = ["diff", "-u", "--minimal", ref, new] getTest :: FilePath -> FilePath -> IO TestTree getTest tmpdir fp = do@@ -91,4 +97,5 @@ res <- applyTemplate templatePath template context case res of Left e -> error e- Right x -> T.writeFile actual $ j' <> ".\n" <> template <> ".\n" <> x+ Right x -> T.writeFile actual $ j' <> ".\n" <> template <> ".\n" <>+ render Nothing x