{-#LANGUAGE OverloadedStrings #-} {-#LANGUAGE FlexibleContexts #-} module Text.Ginger.SimulationTests where import Text.Ginger import Text.Ginger.Html import Test.Tasty import Test.Tasty.HUnit import Data.Default (def) import Data.Text (Text) import qualified Data.Text as Text import Data.Maybe (fromMaybe) import Control.Monad.Writer (WriterT, runWriterT) import Control.Monad.IO.Class (liftIO) import Data.IORef import Data.Monoid import qualified Data.Aeson as JSON import qualified Data.ByteString.Lazy.UTF8 as LUTF8 import qualified Data.ByteString.Lazy as LBS import Data.Time (TimeLocale) simulationTests :: TestTree simulationTests = testGroup "Simulation" [ testCase "Smoke Test" $ mkTestHtml [] [] "Hello" "Hello" , testGroup "Comments" -- There is a comment between the two dashes that should not appear in -- the output. [ testCase "Comment does not appear in output" $ mkTestHtml [] [] "- {# Comments #} -" "- -" ] , testGroup "Dashed limiters eat whitespace" [ testCase "comments" $ mkTestHtml [] [] "- {#- Comment -#} -" "--" , testCase "interpolations" $ mkTestHtml [] [] "- {{- '' -}} -" "--" , testCase "flow" $ mkTestHtml [] [] "- {%- set x=1 -%} -" "--" ] , testGroup "Literals" [ testGroup "Strings" [ testCase "String: \"foobar\"" $ mkTestHtml [] [] "{{ \"foobar\" }}" "foobar" , testCase "String: \"\\r\\n\\t\\b\"" $ mkTestHtml [] [] "{{ \"\\r\\n\\t\\b\" }}" "\r\n\t\b" ] , testGroup "Numbers" [ testCase "123" $ mkTestHtml [] [] "{{ 123 }}" "123" , testCase "3.1415" $ mkTestHtml [] [] "{{ 3.1415 }}" "3.1415" ] , testGroup "Booleans" [ testCase "true" $ mkTestHtml [] [] "{{ true }}" "1" , testCase "false" $ mkTestHtml [] [] "{{ false }}" "" ] , testCase "Null" $ mkTestHtml [] [] "{{ null }}" "" ] , testGroup "Simple list/object constructs" [ testCase "Lists" $ mkTestHtml [] [] "{{ [\"foo\",\"bar\",\"baz\" ] }}" "foobarbaz" , testCase "Nested lists" $ mkTestHtml [] [] "{{ [ \"foo\", \"bar\", [ 1, 2, 3 ], \"baz\" ] }}" "foobar123baz" , testCase "Objects" $ mkTestHtml [] [] "{{ { \"foo\":\"bar\" } }}" "bar" , testCase "Nested object/list constructs" $ mkTestHtml [] [] "{{ { \"foo\":[\"foo\", {\"asdf\" : \"bar\"}, [\"baz\" ]] } }}" "foobarbaz" ] , testGroup "Accessing object/list members" [ testCase "by integer index" $ mkTestHtml [] [] "{{ [ \"foo\", \"bar\" ][1] }}" "bar" , testCase "by string key" $ mkTestHtml [] [] "{{ { \"foo\": \"bar\", \"baz\": \"quux\" }['foo'] }}" "bar" , testCase "by property name" $ mkTestHtml [] [] "{{ { \"foo\": \"bar\", \"baz\": \"quux\" }.foo }}" "bar" , testCase "multi-level mixed" $ mkTestHtml [] [] "{{ { \"foo\": { \"oink\": \"nope\", \"baz\": { \"boop\": [], \"quux\": \"bar\" }}}.foo.baz[\"quux\"] }}" "bar" ] , testGroup "Function calls" -- In order to make sure the correct function is called, we write one -- that stores its argument in an IORef that it closes over, thus -- making it impossible for other functions to modify it; then we -- assert that the IORef contains what we expect. [ testCase "print(\"Hello\")" $ do buf <- newIORef "" let printF :: Function (Run IO Html) printF = \xs -> do liftIO . writeIORef buf . mconcat . map (asText . snd) $ xs return def mkTestHtml [("print", fromFunction printF)] [] "{{ print(\"Hello\") }}" "" actual <- readIORef buf let expected = "Hello" assertEqual "" actual expected , testCase "\"Hello\"|print" $ do buf <- newIORef "" let printF :: Function (Run IO Html) printF = \xs -> do liftIO . writeIORef buf . mconcat . map (asText . snd) $ xs return def mkTestHtml [("print", fromFunction printF)] [] "{{ \"Hello\"|print }}" "" actual <- readIORef buf let expected = "Hello" assertEqual "" actual expected ] , testGroup "Addition" [ testCase "1 + 1 = 2" $ do mkTestHtml [] [] "{{ sum(1, 1) }}" "2" , testCase "1 + 1 = 2" $ do mkTestHtml [] [] "{{ 1 + 1 }}" "2" ] , testGroup "Subtraction" [ testCase "1 - 1 = 0" $ do mkTestHtml [] [] "{{ 1 - 1 }}" "0" ] , testGroup "Concatenation" [ testCase "1 ~ \"foo\" = 1foo" $ do mkTestHtml [] [] "{{ 1 ~ \"foo\" }}" "1foo" ] , testGroup "Multiplication" [ testCase "5 * 5 = 25" $ do mkTestHtml [] [] "{{ 5 * 5 }}" "25" ] , testGroup "Division" [ testCase "24 / 6 = 4" $ do mkTestHtml [] [] "{{ 24 / 6 }}" "4" , testCase "3 / 2 = 1.5" $ do mkTestHtml [] [] "{{ 3 / 2 }}" "1.5" ] , testGroup "Integer Division" [ testCase "24 // 6 = 4" $ do mkTestHtml [] [] "{{ 24 // 6 }}" "4" , testCase "3 // 2 = 1" $ do mkTestHtml [] [] "{{ 3 // 2 }}" "1" ] , testGroup "Modulo" [ testCase "7 % 3 = 2" $ do mkTestHtml [] [] "{{ 7 % 3 }}" "1" ] , testGroup "Iteration" [ testCase "for x in [ \"foo\", \"bar\", \"baz\" ]: " $ do mkTestHtml [] [] "{% for x in [ \"foo\", \"bar\", \"baz\" ] %}<{{x}}>{% endfor %}" "" , testCase "for x in []: else " $ do mkTestHtml [] [] "{% for x in [] %}<{{x}}>{% else %}{% endfor %}" "" , testCase "for x in [a]: else " $ do mkTestHtml [] [] "{% for x in [\"a\"] %}<{{x}}>{% else %}{% endfor %}" "" ] , testGroup "The `loop` auto-variable" [ testCase "loop.cycle" $ do mkTestHtml [] [] ( "{% for x in [\"foo\", \"bar\", \"baz\"] recursive -%}" ++ "({{ loop.cycle(\"red\", \"green\") }}/{{ x }})" ++ "{%- endfor %}" ) "(red/foo)(green/bar)(red/baz)" , testCase "recursive loops" $ do mkTestHtml [] [] ( "{% for k, x in {\"a\":{\"b\":null,\"c\":{\"d\":null}}} -%}" ++ "{{ k }}@{{ loop.depth }}({{ loop(x) }})" ++ "{%- endfor %}" :: String ) "a@1(b@2()c@2(d@3()))" ] , testGroup "Conditionals" [ testCase "if true then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if true %}yes{% else %}no{% endif %}" "yes" , testCase "if false then \"yes\" else if false then \"maybe\" else \"no\"" $ do mkTestHtml [] [] "{% if false %}yes{% elif false %}maybe{% else %}no{% endif %}" "no" , testCase "if false then \"yes\" else if true then \"maybe\" else \"no\"" $ do mkTestHtml [] [] "{% if false %}yes{% elif true %}maybe{% else %}no{% endif %}" "maybe" ] , testGroup "Switch" [ testCase "switch 1 of 1, 2, default" $ do mkTestHtml [] [] ( "{% switch 1 %}\n" ++ "{% case 1 %}One{% endcase %}\n" ++ "{% case 2 %}Two{% endcase %}\n" ++ "{% default %}Default{% enddefault %}\n" ++ "{% endswitch %}\n" ) "One" , testCase "switch 1 of 1, 2" $ do mkTestHtml [] [] ( "{% switch 1 %}\n" ++ "{% case 1 %}One{% endcase %}\n" ++ "{% case 2 %}Two{% endcase %}\n" ++ "{% endswitch %}\n" ) "One" ] , testGroup "Comparisons" [ testCase "if 1 == 1 then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if (1 == 1) %}yes{% else %}no{% endif %}" "yes" , testCase "if 1 > 0 then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if (1 > 0) %}yes{% else %}no{% endif %}" "yes" , testCase "if 1 > null then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if (1 > null) %}yes{% else %}no{% endif %}" "no" , testCase "if 1 < 2 then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if (1 < 2) %}yes{% else %}no{% endif %}" "yes" , testCase "if null < 1 then \"yes\" else \"no\"" $ do mkTestHtml [] [] "{% if (null < 1) %}yes{% else %}no{% endif %}" "no" ] , testGroup "Boolean AND" [ testCase "AND (both)" $ do mkTestHtml [] [] "{% if 1 && 2 %}yes{% else %}no{% endif %}" "yes" , testCase "AND (only one)" $ do mkTestHtml [] [] "{% if 1 && 0 %}yes{% else %}no{% endif %}" "no" , testCase "AND (neither)" $ do mkTestHtml [] [] "{% if 0 && 0 %}yes{% else %}no{% endif %}" "no" ] , testGroup "Boolean AND" [ testCase "OR (both)" $ do mkTestHtml [] [] "{% if 1 || 2 %}yes{% else %}no{% endif %}" "yes" , testCase "OR (only one)" $ do mkTestHtml [] [] "{% if 1 || 0 %}yes{% else %}no{% endif %}" "yes" , testCase "OR (either)" $ do mkTestHtml [] [] "{% if 0 || 0 %}yes{% else %}no{% endif %}" "no" ] , testGroup "Slicing brackets" [ testCase "from/to, both positive" $ do mkTestHtml [] [] "{{ 'abcdef'[1:2] }}" "bc" , testCase "from/to, from negative" $ do mkTestHtml [] [] "{{ 'abcdef'[-3:2] }}" "de" , testCase "from/to, to implicit" $ do mkTestHtml [] [] "{{ 'abcdef'[1:] }}" "bcdef" , testCase "from/to, from implicit" $ do mkTestHtml [] [] "{{ 'abcdef'[:2] }}" "ab" ] , testGroup "Built-in filters/functions" [ testCase "\"abs\"" $ do mkTestHtml [] [] "{{ -2|abs }}" "2" , testGroup "\"any\"" [ testCase "\"any\" (both)" $ do mkTestHtml [] [] "{% if any(1, 1, true) %}yes{% else %}no{% endif %}" "yes" , testCase "\"any\" (just one)" $ do mkTestHtml [] [] "{% if any(0, 1, false) %}yes{% else %}no{% endif %}" "yes" , testCase "\"any\" (neither)" $ do mkTestHtml [] [] "{% if any(0, 0, false) %}yes{% else %}no{% endif %}" "no" ] , testGroup "\"all\"" [ testCase "\"all\" (both)" $ do mkTestHtml [] [] "{% if all(1, 1, true) %}yes{% else %}no{% endif %}" "yes" , testCase "\"all\" (just one)" $ do mkTestHtml [] [] "{% if all(0, 1, false) %}yes{% else %}no{% endif %}" "no" , testCase "\"all\" (neither)" $ do mkTestHtml [] [] "{% if all(0, 0, false) %}yes{% else %}no{% endif %}" "no" ] , testGroup "\"ceil\"" [ testCase "14.1" $ do mkTestHtml [] [] "{{ 14.1|ceil }}" "15" , testCase "-14.1" $ do mkTestHtml [] [] "{{ -14.1|ceil }}" "-14" , testCase "-14.8" $ do mkTestHtml [] [] "{{ -14.8|ceil }}" "-14" ] , testCase "\"capitalize\"" $ do mkTestHtml [] [] "{{ \"this is the end of the world\"|capitalize }}" "This is the end of the world" , testCase "\"upper\"" $ do mkTestHtml [] [] "{{ \"this is the end of the world\"|upper }}" "THIS IS THE END OF THE WORLD" , testCase "\"lower\"" $ do mkTestHtml [] [] "{{ \"This is the END OF THE WORLD\"|lower }}" "this is the end of the world" , testGroup "\"center\"" [ testCase "extra space" $ do mkTestHtml [] [] "{{ \"asdf\"|center(12) }}" " asdf " , testCase "no extra space" $ do mkTestHtml [] [] "{{ \"foobar\"|center(2) }}" "foobar" ] , testCase "\"concat\"" $ do mkTestHtml [] [] "{{ [\"hello\", \"world\"]|concat }}" "helloworld" , testGroup "\"contains\"" [ testCase "single match" $ do mkTestHtml [] [] "{% if ['hello', 'world']|contains('hello') %}yes{% else %}no{% endif %}" "yes" , testCase "multi match" $ do mkTestHtml [] [] "{% if ['hello', 'world']|contains('hello', 'world') %}yes{% else %}no{% endif %}" "yes" , testCase "non-match" $ do mkTestHtml [] [] "{% if ['hello', 'world']|contains('hello', 'you', 'world') %}yes{% else %}no{% endif %}" "no" ] , testGroup "\"date\"" [ testCase "format a date" $ do mkTestHtml [] [] "{{ {'year':2015, 'month':6, 'day':13}|date('%Y-%m-%d') }}" "2015-06-13" , testCase "format a list as a date" $ do mkTestHtml [] [] "{{ [2015, 6, 13, 12, 45, 21]|date('%Y-%m-%d') }}" "2015-06-13" , testCase "format a 5-element list as a date" $ do mkTestHtml [] [] "{{ [2015, 6, 13, 12, 45]|date('%Y-%m-%d') }}" "2015-06-13" , testCase "format a 3-element list as a date" $ do mkTestHtml [] [] "{{ [2015, 6, 13]|date('%Y-%m-%d') }}" "2015-06-13" , testCase "use correct default time (noon) with 3-element list" $ do mkTestHtml [] [] "{{ [2015, 6, 13]|date('%H:%M:%S') }}" "12:00:00" , testCase "format a string as a date" $ do mkTestHtml [] [] "{{ '2015-06-13 12:05:43'|date('%Y-%m-%d') }}" "2015-06-13" , testCase "format a string as a date (JSON-style formatting)" $ do mkTestHtml [] [] "{{ '2015-06-13T12:05:43Z'|date('%Y-%m-%d %H:%M:%S') }}" "2015-06-13 12:05:43" , testCase "format a string as a time-of-day" $ do mkTestHtml [] [] "{{ '2015-06-13 12:05:43'|date('%H-%M-%S') }}" "12-05-43" , testCase "format a string as a date, with timezone" $ do mkTestHtml [] [] "{{ '2015-06-13 12:05:43-05:00'|date('%Y-%m-%d %z') }}" "2015-06-13 -0500" , testCase "format a local-time string as a date + time, with explicit timezone" $ do mkTestHtml [] [] "{{ '2015-06-13 12:05:43'|date('%Y-%m-%d %H:%M %z', tz='+0200') }}" "2015-06-13 12:05 +0200" , testCase "format a zoned-time string as a date, with explicit timezone" $ do mkTestHtml [] [] "{{ '2015-06-13 12:05:43-05:00'|date('%Y-%m-%d %H:%M %z', tz='+0200') }}" "2015-06-13 19:05 +0200" , testCase "use a custom locale" $ do sillyLocale <- loadSillyLocale mkTestHtml [("silly", toGVal (sillyLocale :: Maybe JSON.Value))] [] "{{ '2015-06-13 12:05:43'|date('%c', locale=silly) }}" "It be The Day Of Saturn, in the year 15, the clock striketh 12" ] , testGroup "\"default\"" [ testCase "trigger default" $ do mkTestHtml [] [] "{{ 0|default(\"hi\") }}" "hi" , testCase "use truthy value" $ do mkTestHtml [] [] "{{ \"hi\"|default(\"nope\") }}" "hi" ] , testCase "\"difference\"" $ do mkTestHtml [] [] "{{ difference(5,2) }}" "3" , testGroup "\"dictsort\"" [ testCase "by key" $ do mkTestHtml [] [] "{{ dictsort({4:4, 1:5}, by='key') }}" "54" , testCase "by value" $ do mkTestHtml [] [] "{{ dictsort({4:4, 1:5}, by='value') }}" "45" ] , testGroup "\"escape\"" [ testCase "single item" $ do mkTestHtml [] [] "{{ escape('<')|raw }}" "<" , testCase "multiple items" $ do mkTestHtml [] [] "{{ escape('<', '>')|raw }}" "<>" ] , testGroup "\"equals\"" [ testCase "all equal" $ do mkTestHtml [] [] "{% if equals(1, 1, 1) %}yes{% else %}no{% endif %}" "yes" , testCase "some equal" $ do mkTestHtml [] [] "{% if equals(1, 1, 2) %}yes{% else %}no{% endif %}" "no" ] , testGroup "\"eval\"" [ testCase "simple" $ do mkTestHtml [] [] "{{ eval('{% set x = 1 %}{{x}}') }}" "1" , testCase "with extra state" $ do mkTestHtml [] [] "{{ eval('{{x}}', { 'x': 1 }) }}" "1" , testCase "outside state does not bleed into eval()" $ do mkTestHtml [] [] "{% set x = 1 %}{{ eval('{{x}}') }}" "" , testCase "standard functions available inside eval" $ do mkTestHtml [] [] "{{ eval(\"{{'foobar'|capitalize()}}\") }}" "Foobar" ] , testGroup "\"filesizeformat\"" [ testCase "bytes" $ do mkTestHtml [] [] "{{ 100|filesizeformat }}" "100 B" , testCase "kilobytes" $ do mkTestHtml [] [] "{{ 12000|filesizeformat }}" "12 kB" , testCase "megabytes" $ do mkTestHtml [] [] "{{ 1500000|filesizeformat }}" "1.5 MB" , testCase "bytes (2-based)" $ do mkTestHtml [] [] "{{ 100|filesizeformat(true) }}" "100 B" , testCase "kibibytes" $ do mkTestHtml [] [] "{{ 12000|filesizeformat(true) }}" "11.7 kiB" , testCase "mebibytes" $ do mkTestHtml [] [] "{{ 1500000|filesizeformat(true) }}" "1.4 MiB" ] , testGroup "\"filter\"" [ testCase "simple case" $ do mkTestHtml [] [] "{{ [1, 0, 3]|filter(int) }}" "13" , testCase "with extra argument" $ do mkTestHtml [] [] "{{ [1, 2, 3]|filter(greater, 2) }}" "3" ] , testGroup "\"format\"" [ testCase "jinja.pocoo.org example" $ do mkTestHtml [] [] "{{ \"%s - %s\"|format('Hello?', 'Foo!') }}" "Hello? - Foo!" ] , testGroup "\"map\"" [ testCase "map function over list" $ do mkTestHtml [] [] "{{ map(['FOO', 'BAR'], lower) }}" "foobar" , testCase "map function over dictionary" $ do mkTestHtml [] [] "{{ map({'foo': 'bar'}, upper)['foo'] }}" "BAR" , testCase "map to extract attribute" $ do mkTestHtml [] [] "{{ map([{'name':'foo'}, {'name': 'bar'}], attribute='name') }}" "foobar" ] , testGroup "\"not-equals\"" [ testCase "all equal" $ do mkTestHtml [] [] "{% if nequals(1, 1, 1) %}yes{% else %}no{% endif %}" "no" , testCase "not all equal" $ do mkTestHtml [] [] "{% if nequals(1, 1, 2) %}yes{% else %}no{% endif %}" "yes" ] , testGroup "\"floor\"" [ testCase "14.1" $ do mkTestHtml [] [] "{{ 14.1|floor }}" "14" , testCase "14.8" $ do mkTestHtml [] [] "{{ 14.8|floor }}" "14" , testCase "-14.1" $ do mkTestHtml [] [] "{{ -14.1|floor }}" "-15" , testCase "-14.8" $ do mkTestHtml [] [] "{{ -14.8|floor }}" "-15" ] , testGroup "\"int\"" [ testCase "14.1" $ do mkTestHtml [] [] "{{ 14.1|int }}" "14" , testCase "14.8" $ do mkTestHtml [] [] "{{ 14.8|int }}" "14" , testCase "-14.1" $ do mkTestHtml [] [] "{{ -14.1|int }}" "-14" , testCase "-14.8" $ do mkTestHtml [] [] "{{ -14.8|int }}" "-14" ] -- \"int_ratio\" -- TODO -- \"iterable\" -- TODO , testCase "\"length\"" $ do mkTestHtml [] [] "{{ [1,2,3]|length }}" "3" -- \"modulo\" -- \"num\" -- TODO -- \"printf\" , testGroup "\"printf\"" [ testCase "%s, passed as int" $ do mkTestHtml [] [] "{{ printf(\"%i\", 1) }}" "1" , testCase "%s, passed as string" $ do mkTestHtml [] [] "{{ printf(\"%i\", \"1\") }}" "1" , testCase "%i, passed as float" $ do mkTestHtml [] [] "{{ printf(\"%i\", 1.3) }}" "1" , testCase "%f, passed as int" $ do mkTestHtml [] [] "{{ printf(\"%f\", 1) }}" "1.0" , testCase "%.3f, passed as int" $ do mkTestHtml [] [] "{{ printf(\"%.3f\", 1) }}" "1.000" , testCase "%s, string" $ do mkTestHtml [] [] "{{ printf(\"%s\", \"Hello\") }}" "Hello" ] , testCase "\"product\"" $ do mkTestHtml [] [] "{{ product(1,2,3) }}" "6" , testCase "\"ratio\"" $ do mkTestHtml [] [] "{{ ratio(6, 1.5, 2) }}" "2" , testGroup "\"round\"" [ testCase "14.1" $ do mkTestHtml [] [] "{{ 14.1|round }}" "14" , testCase "14.8" $ do mkTestHtml [] [] "{{ 14.8|round }}" "15" , testCase "-14.1" $ do mkTestHtml [] [] "{{ -14.1|round }}" "-14" , testCase "-14.8" $ do mkTestHtml [] [] "{{ -14.8|round }}" "-15" ] -- \"show\" -- TODO , testCase "\"str\"" $ do mkTestHtml [] [] "{{ str(123) }}" "123" , testCase "\"sum\"" $ do mkTestHtml [] [] "{{sum(1, 2, 3)}}" "6" , testGroup "\"truncate\"" [ testCase "14.1" $ do mkTestHtml [] [] "{{ 14.1|truncate }}" "14" , testCase "14.8" $ do mkTestHtml [] [] "{{ 14.8|truncate }}" "14" , testCase "-14.1" $ do mkTestHtml [] [] "{{ -14.1|truncate }}" "-14" , testCase "-14.8" $ do mkTestHtml [] [] "{{ -14.8|truncate }}" "-14" ] , testCase "\"urlencode\"" $ do mkTestHtml [] [] "{{ \"a/b c\"|urlencode }}" "a%2Fb%20c" , testGroup "\"sort\"" [ testCase "simple" $ do mkTestHtml [] [] "{{ [2,3,1]|sort }}" "123" , testCase "reverse" $ do mkTestHtml [] [] "{{ [2,3,1]|sort(reverse=true) }}" "321" , testCase "sort by key" $ do mkTestHtml [] [] "{{ ([ {\"age\":30, \"name\":\"zzz\"}, {\"age\":41, \"name\":\"aaa\"} ]|sort(by=\"name\"))[0]['name'] }}" "aaa" , testCase "sort by key, reverse" $ do mkTestHtml [] [] "{{ ([ {\"age\":30, \"name\":\"zzz\"}, {\"age\":41, \"name\":\"aaa\"} ]|sort(by=\"age\", reverse=true))[0]['name'] }}" "aaa" , testCase "sort dictionary by keys" $ do mkTestHtml [] [] "{{ {'foo': 1, 'bar': 2}|sort(by='__key') }}" "21" , testCase "sort by a projection function" $ do mkTestHtml [] [] "{{ ['foo', 'bar', 'quux']|sort(by=(str) -> str[1:]) }}" "barfooquux" , testCase "sort by a path" $ do mkTestHtml [] [] "{{ [{'main':{'sub': 2}, 'msg':'no'},{'main':{'sub': 1}, 'msg':'yes'}]|sort(by=['main', 'sub']) }}" "1yes2no" ] , testGroup "\"slice\"" [ testCase "full positional args" $ do mkTestHtml [] [] "{{ [1, 2, 3, 4, 5]|slice(1,3) }}" "234" , testCase "implicit 'to end'" $ do mkTestHtml [] [] "{{ [1, 2, 3, 4, 5]|slice(1) }}" "2345" , testCase "full named args" $ do mkTestHtml [] [] "{{ [1, 2, 3, 4, 5]|slice(length=3,start=1) }}" "234" , testCase "negative offset" $ do mkTestHtml [] [] "{{ [1, 2, 3, 4, 5]|slice(start=-1) }}" "5" , testCase "call on string subject" $ do mkTestHtml [] [] "{{ \"12345\"|slice(1,3) }}" "234" ] , testGroup "\"replace\"" [ testCase "simple case" $ do mkTestHtml [] [] "{{ replace('foobar', 'a', 'e') }}" "foober" , testCase "multiple replacements" $ do mkTestHtml [] [] "{{ replace('foobar', 'o', 'e') }}" "feebar" , testCase "longer replacements" $ do mkTestHtml [] [] "{{ replace('foobar', 'oo', 'e') }}" "febar" , testCase "deletion" $ do mkTestHtml [] [] "{{ replace('foobar', 'o') }}" "fbar" ] ] , testGroup "Setting variables" [ testCase "plain" $ do mkTestHtml [] [] "{% set x = \"world\" %}Hello, {{ x }}!" "Hello, world!" , testCase "self-referential" $ do mkTestHtml [] [] "{% set x = { \"foo\":\"bar\" } %}{% set x = x[\"foo\"] %}Hello, {{ x }}!" "Hello, bar!" ] , testGroup "HTML encoding" [ testCase "no encoding outside of constructs" $ do mkTestHtml [] [] "Ampersand is '&'." "Ampersand is '&'." , testCase "auto-encode inside interpolations" $ do mkTestHtml [] [] "{{ \"amp is '&'.\" }}" "<foo bar="baz"/>amp is '&'</foo>." , testCase "raw filter bypasses encoding" $ do mkTestHtml [] [] "{{ \"&\"|raw }}" "&" ] , testGroup "Includes" [ testCase "include plain" $ do mkTestHtml [] [("./features-included.html", "This is an included template")] "{% include 'features-included.html' %}" "This is an included template" , testCase "include with a variable" $ do mkTestHtml [] [("./features-included.html", "Hello, {{ user }}!")] "{% set user='world' %}{% include 'features-included.html' %}" "Hello, world!" , testCase "include referencing an included variable" $ do mkTestHtml [] [("./features-included.html", "{% set user = 'foobar' %}")] "{% include 'features-included.html' %}Hello, {{ user }}!" "Hello, foobar!" ] , testGroup "Explicit Local Scopes" [ testCase "baseline" $ do mkTestHtml [] [] "{% set bedazzle = \"no\" %}{{ bedazzle }}" "no" , testCase "inside local scope" $ do mkTestHtml [] [] "{% set bedazzle = \"no\" %}{% scope %}{% set bedazzle = \"ya\" %}{{ bedazzle }}{% endscope %}" "ya" , testCase "after exiting local scope" $ do mkTestHtml [] [] "{% set bedazzle = \"no\" %}{% scope %}{% set bedazzle = \"ya\" %}{% endscope %}{{ bedazzle }}" "no" ] , testGroup "Indentation" [ testCase "stripping leading spaces" $ do mkTestHtml [] [] (unlines [ "{% indent %}" , " aaaaa" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , "aaaaa" ]) , testCase "explicit indent string" $ do mkTestHtml [] [] (unlines [ "{% indent %}" , " aaaaa" , "{% indent ' ' %}" , " aaaaa" , "{% endindent %}" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , " aaaaa" , "aaaaa" ]) , testCase "implicit indent string" $ do mkTestHtml [] [] (unlines [ "{% indent %}" , " aaaaa" , "{% indent %}" , " aaaaa" , "{% endindent %}" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , " aaaaa" , "aaaaa" ]) , testCase "explicit non-whitespace indent string" $ do mkTestHtml [] [] (unlines [ "{% indent %}" , " aaaaa" , "{% indent '--- ' %}" , " aaaaa" , "{% endindent %}" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , "--- aaaaa" , "aaaaa" ]) , testCase "explicit indent string from more complex expression" $ do mkTestHtml [] [] (unlines [ "{% indent %}" , " aaaaa" , "{% indent (17 + 4) ~ ' '%}" , " aaaaa" , "{% endindent %}" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , "21 aaaaa" , "aaaaa" ]) , testCase "discarding level-0 indents" $ do mkTestHtml [] [] (unlines [ "{% indent 'nope' %}" , " aaaaa" , "{% indent %}" , " aaaaa" , "{% endindent %}" , " aaaaa" , "{% endindent %}" ]) (Text.unlines [ "aaaaa" , " aaaaa" , "aaaaa" ]) , testCase "indentation levels inherited at runtime (dynamic)" $ do mkTestHtml [] [] (unlines [ "{%- macro foobar() %}" , "{% indent ' ' %}" , "
" , "{% indent ' ' %}" , "

Hello!

" , "{% endindent %}" , "
" , "{% endindent %}" , "{% endmacro -%}" , "" , "{% indent '' %}" , "" , "{{ foobar() }}" , "" , "{% endindent %}" ]) (Text.unlines [ "" , "
" , "

Hello!

" , "
" , "" ]) ] , testGroup "Macros" [ testCase "simple" $ do mkTestHtml [] [] "{% macro foobar -%}baz{%- endmacro %}{{ foobar() }}" "baz" , testCase "with args" $ do mkTestHtml [] [] "{% macro foobar2(baz) -%}{{ baz }}oo{%- endmacro %}{{ foobar2(boink=\"nope\", baz=\"blabber\") }}" "blabberoo" ] , testGroup "Lambdas" [ testCase "single arg" $ do mkTestHtml [] [] "{{ ((x) -> x * x)(2) }}" "4" , testCase "two args" $ do mkTestHtml [] [] "{{ ((greeting, name) -> greeting ~ \", \" ~ name ~ \"!\")(\"Hello\", \"world\") }}" "Hello, world!" ] , testGroup "Ternary operator" [ testCase "C syntax" $ do mkTestHtml [] [] "{{ 1 ? \"yes\" : \"no\" }}" "yes" , testCase "python syntax" $ do mkTestHtml [] [] "{{ \"yes\" if 1 else \"no\" }}" "yes" , testCase "C syntax, nested, true/false" $ do mkTestHtml [] [] "{{ true ? false ? \"a\" : \"b\" : \"c\" }}" "b" , testCase "Python syntax, nested, true/false" $ do mkTestHtml [] [] "{{ \"a\" if false else \"b\" if true else \"c\" }}" "b" , testCase "C syntax, nested, true/true" $ do mkTestHtml [] [] "{{ true ? true ? \"a\" : \"b\" : \"c\" }}" "a" , testCase "Python syntax, nested, true/true" $ do mkTestHtml [] [] "{{ false ? true ? \"a\" : \"b\" : \"c\" }}" "c" ] , testGroup "Call syntax" [ testCase "\"caller\"" $ do mkTestHtml [] [] "{% macro foobar3(a) -%}{{ a }}({{ caller(\"asdf\") }}){%- endmacro %}{% call (a) foobar3(\"hey\") %}<{{a}}>{% endcall %}" "hey()" ] , testGroup "Inheritance" [ testCase "inheritance" $ do mkTestHtml [] [ ("./inherit-child.html", "{% extends \"inherit-parent.html\" %}{% block test -%}This is right.{% endblock %}") , ("./inherit-parent.html", "{%- block test -%}This is wrong.{% endblock -%}") ] "{% include \"inherit-child.html\" %}" "This is right." , testCase "multi-tier inheritance" $ do mkTestHtml [] [ ( "./inherit-parent.html" , "{%- extends \"inherit-grandparent.html\" %}" ++ "{%- block outer -%}Outer{%- block inner -%}{%- endblock -%}{%- endblock -%}" ) , ( "./inherit-grandparent.html" , "*{% block outer -%}{%- endblock %}*" ) ] ( "{%- extends \"inherit-parent.html\" -%}" ++ "{%- block inner %} Space{%- endblock -%}" ) "*Outer Space*" ] , testGroup "Non-HTML Output" [ testCase "text" $ do mkTestText [] [] "<>{{ '<>' }}<>" "<><><>" , testCase "json" $ do mkTestJSON [] [] "{{ '<>' }}{{ 1 }}{{ {'foo': true} }}" "[\"<>\",1,{\"foo\":true}]" ] , testGroup "Script mode" [ testCase "empty script block" $ mkTestHtml [] [] "{% script %}{% endscript %}" "" , testGroup "comments" [ testCase "simple" $ mkTestHtml [] [] "{% script %} # this is a comment\n{% endscript %}" "" , testCase "multiple" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "23; # this is a comment" , " ## this is a comment, too" , "{% endscript %}" ]) "" , testCase "inside expressions" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "23 # this is a comment" , " ## this is a comment, too" , " ;" , "{% endscript %}" ]) "" ] , testCase "echo" $ mkTestHtml [] [] "{% script %}echo('Hi!');{% endscript %}" "Hi!" , testCase "expression statement" $ mkTestHtml [] [] "{% script %}12 + 15;{% endscript %}" "" , testCase "grouped statements" $ mkTestHtml [] [] "{% script %}{ \n 1; 2; 3; 4; }{% endscript %}" "" , testCase "if" $ mkTestHtml [] [] "{% script %}if (true) { echo('Hi!'); }{% endscript %}" "Hi!" , testCase "if/else" $ mkTestHtml [] [] "{% script %}if (false) echo ('nope'); else { echo('Hi!'); }{% endscript %}" "Hi!" , testCase "switch" $ mkTestHtml [] [] "{% script %}switch ('hi') { case 'hi': echo('Hello'); case 'no': echo('Nope'); default: echo('Default'); }{% endscript %}" "Hello" , testCase "for" $ mkTestHtml [] [] "{% script %}for (i in [1,2,3]) echo(i);{% endscript %}" "123" , testCase "for/else (loop)" $ mkTestHtml [] [] "{% script %}for (i in [1,2,3]) echo(i); else echo('none');{% endscript %}" "123" , testCase "for/else (recover)" $ mkTestHtml [] [] "{% script %}for (i in null) echo(i); else echo('none');{% endscript %}" "none" , testCase "set" $ mkTestHtml [] [] "{% script %}set a = 'Hi' ~ '!'; echo(a);{% endscript %}" "Hi!" , testCase "include" $ do mkTestHtml [] [("./features-included.html", "This is an included template")] "{% script %}include('features-included.html');{% endscript %}" "This is an included template" , testCase "macro" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "macro greet(name) {" , "echo('Hello, ');" , "echo(name);" , "}" , "" , "echo(greet('tobias'));" , "{% endscript %}" ]) "Hello, tobias" , testGroup "Script statment blocks" [ testCase "baseline" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "echo(bedazzle);" , "{% endscript %}" ]) "no" , testCase "inside local scope" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "{" , " set bedazzle = 'ya';" , " echo(bedazzle);" , "}" , "{% endscript %}" ]) "ya" , testCase "after exiting block" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "{" , " set bedazzle = 'ya';" , "}" , "echo(bedazzle);" , "{% endscript %}" ]) "ya" ] , testGroup "Explicit Local Scopes" [ testCase "baseline" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "echo(bedazzle);" , "{% endscript %}" ]) "no" , testCase "inside local scope" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "scope {" , " set bedazzle = 'ya';" , " echo(bedazzle);" , "}" , "{% endscript %}" ]) "ya" , testCase "after exiting local scope" $ do mkTestHtml [] [] (unlines [ "{% script %}" , "set bedazzle = 'no';" , "scope {" , " set bedazzle = 'ya';" , "}" , "echo(bedazzle);" , "{% endscript %}" ]) "no" ] ] , testGroup "do expressions" [ testCase "single statement" $ do mkTestHtml [] [] "{{ do 'hello'; }}" "hello" , testCase "statement block" $ do mkTestHtml [] [] "{{ do { 'hello'; 'world'; } }}" "world" ] ] mkTestHtml :: [(VarName, GVal (Run IO Html))] -> [(FilePath, String)] -> String -> Text -> Assertion mkTestHtml = mkTest makeContextHtmlM htmlSource mkTestText :: [(VarName, GVal (Run IO Text))] -> [(FilePath, String)] -> String -> Text -> Assertion mkTestText = mkTest makeContextTextM id mkTestJSON :: [(VarName, GVal (Run IO [JSON.Value]))] -> [(FilePath, String)] -> String -> Text -> Assertion mkTestJSON = mkTest (\l w -> makeContextM' l w toJSONSingleton Nothing) encodeText where toJSONSingleton = (:[]) . JSON.toJSON encodeText :: JSON.ToJSON a => a -> Text encodeText = Text.pack . LUTF8.toString . JSON.encode mkTest :: (Monoid a, ToGVal (Run IO a) a) => ((VarName -> Run IO a (GVal (Run IO a))) -> (a -> IO ()) -> GingerContext IO a) -- ^ mkContextM flavor -> (a -> Text) -- ^ Convert a to Text for output -> [(VarName, GVal (Run IO a))] -- ^ Context dictionary -> [(FilePath, String)] -- ^ Lookup table for includes -> String -- ^ Template source -> Text -- ^ Expected output -> Assertion mkTest mContext valToText contextDict includeLookup src expected = do let resolver srcName = return $ lookup srcName includeLookup template <- either (fail . show) return =<< parseGinger resolver Nothing src output <- newIORef mempty let write h = modifyIORef output (<> h) let context = mContext (\key -> return $ fromMaybe def (lookup key contextDict)) write runGingerT context (optimize template) actual <- valToText <$> readIORef output assertEqual "" expected actual loadSillyLocale :: IO (Maybe JSON.Value) loadSillyLocale = do JSON.decode <$> LBS.readFile "test/fixtures/silly-locale.json"