├── .credo.exs ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── README.md ├── bench └── basic_bench.exs ├── lib ├── errors │ └── errors.ex ├── liquid.ex ├── liquid │ ├── appointer.ex │ ├── block.ex │ ├── condition.ex │ ├── context.ex │ ├── expression.ex │ ├── file_system.ex │ ├── filters.ex │ ├── include.ex │ ├── parse.ex │ ├── range_lookup.ex │ ├── registers.ex │ ├── render.ex │ ├── supervisor.ex │ ├── tag.ex │ ├── tags │ │ ├── assign.ex │ │ ├── capture.ex │ │ ├── case.ex │ │ ├── comment.ex │ │ ├── cycle.ex │ │ ├── decrement.ex │ │ ├── for_else.ex │ │ ├── if_else.ex │ │ ├── increment.ex │ │ ├── raw.ex │ │ ├── table_row.ex │ │ └── unless.ex │ ├── template.ex │ ├── utils.ex │ └── variable.ex └── protocols │ ├── blank.ex │ └── matcher.ex ├── mix.exs ├── mix.lock └── test ├── integration └── cases_test.exs ├── liquid ├── block_test.exs ├── capture_test.exs ├── condition_test.exs ├── custom_filter_test.exs ├── custom_tag_test.exs ├── fetch_attribute_test.exs ├── file_system_test.exs ├── filter_test.exs ├── global_filter_test.exs ├── regex_test.exs ├── strict_parse_test.exs ├── template_test.exs └── variable_test.exs ├── liquid_test.exs ├── tags ├── assign_test.exs ├── blank_test.exs ├── case_test.exs ├── for_else_tag_test.exs ├── if_else_test.exs ├── include_test.exs ├── increment_test.exs ├── raw_test.exs ├── standard_tag_test.exs ├── statements_test.exs ├── table_row_test.exs └── unless_test.exs ├── templates ├── complex │ └── 01 │ │ ├── input.liquid │ │ └── output.html ├── db.json ├── liquid.rb ├── medium │ ├── 01 │ │ ├── input.liquid │ │ └── output.html │ ├── 02 │ │ ├── input.liquid │ │ └── output.html │ └── 03 │ │ ├── input.liquid │ │ └── output.html └── simple │ ├── 01 │ ├── input.liquid │ └── output.html │ ├── 02 │ ├── input.liquid │ └── output.html │ └── 03 │ ├── input.liquid │ └── output.html └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "test/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: true, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | # 52 | ## Consistency Checks 53 | # 54 | {Credo.Check.Consistency.ExceptionNames}, 55 | {Credo.Check.Consistency.LineEndings}, 56 | {Credo.Check.Consistency.ParameterPatternMatching}, 57 | {Credo.Check.Consistency.SpaceAroundOperators}, 58 | {Credo.Check.Consistency.SpaceInParentheses}, 59 | {Credo.Check.Consistency.TabsOrSpaces}, 60 | 61 | # 62 | ## Design Checks 63 | # 64 | # You can customize the priority of any check 65 | # Priority values are: `low, normal, high, higher` 66 | # 67 | {Credo.Check.Design.AliasUsage, priority: :low}, 68 | # For some checks, you can also set other parameters 69 | # 70 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 71 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 72 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 73 | # 74 | {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, 75 | # You can also customize the exit_status of each check. 76 | # If you don't want TODO comments to cause `mix credo` to fail, just 77 | # set this value to 0 (zero). 78 | # 79 | {Credo.Check.Design.TagTODO, exit_status: 2}, 80 | {Credo.Check.Design.TagFIXME}, 81 | 82 | # 83 | ## Readability Checks 84 | # 85 | {Credo.Check.Readability.FunctionNames}, 86 | {Credo.Check.Readability.LargeNumbers}, 87 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120}, 88 | {Credo.Check.Readability.ModuleAttributeNames}, 89 | {Credo.Check.Readability.ModuleDoc}, 90 | {Credo.Check.Readability.ModuleNames}, 91 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, 92 | {Credo.Check.Readability.ParenthesesInCondition}, 93 | {Credo.Check.Readability.PredicateFunctionNames}, 94 | {Credo.Check.Readability.PreferImplicitTry}, 95 | {Credo.Check.Readability.RedundantBlankLines}, 96 | {Credo.Check.Readability.StringSigils}, 97 | {Credo.Check.Readability.TrailingBlankLine}, 98 | {Credo.Check.Readability.TrailingWhiteSpace}, 99 | {Credo.Check.Readability.VariableNames}, 100 | {Credo.Check.Readability.Semicolons}, 101 | {Credo.Check.Readability.SpaceAfterCommas}, 102 | 103 | # 104 | ## Refactoring Opportunities 105 | # 106 | {Credo.Check.Refactor.DoubleBooleanNegation}, 107 | {Credo.Check.Refactor.CondStatements}, 108 | {Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 6}, 109 | {Credo.Check.Refactor.FunctionArity}, 110 | {Credo.Check.Refactor.LongQuoteBlocks}, 111 | {Credo.Check.Refactor.MatchInCondition}, 112 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 113 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 114 | {Credo.Check.Refactor.Nesting}, 115 | {Credo.Check.Refactor.PipeChainStart, 116 | excluded_argument_types: [:atom, :binary, :fn, :keyword], excluded_functions: []}, 117 | {Credo.Check.Refactor.UnlessWithElse}, 118 | 119 | # 120 | ## Warnings 121 | # 122 | {Credo.Check.Warning.BoolOperationOnSameValues}, 123 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, 124 | {Credo.Check.Warning.IExPry}, 125 | {Credo.Check.Warning.IoInspect}, 126 | {Credo.Check.Warning.LazyLogging}, 127 | {Credo.Check.Warning.OperationOnSameValues}, 128 | {Credo.Check.Warning.OperationWithConstantResult}, 129 | {Credo.Check.Warning.UnusedEnumOperation}, 130 | {Credo.Check.Warning.UnusedFileOperation}, 131 | {Credo.Check.Warning.UnusedKeywordOperation}, 132 | {Credo.Check.Warning.UnusedListOperation}, 133 | {Credo.Check.Warning.UnusedPathOperation}, 134 | {Credo.Check.Warning.UnusedRegexOperation}, 135 | {Credo.Check.Warning.UnusedStringOperation}, 136 | {Credo.Check.Warning.UnusedTupleOperation}, 137 | {Credo.Check.Warning.RaiseInsideRescue}, 138 | 139 | # 140 | # Controversial and experimental checks (opt-in, just remove `, false`) 141 | # 142 | {Credo.Check.Refactor.ABCSize}, 143 | {Credo.Check.Refactor.AppendSingleItem, false}, 144 | {Credo.Check.Refactor.VariableRebinding, false}, 145 | {Credo.Check.Warning.MapGetUnsafePass, false}, 146 | {Credo.Check.Consistency.MultiAliasImportRequireUse}, 147 | 148 | # 149 | # Deprecated checks (these will be deleted after a grace period) 150 | # 151 | {Credo.Check.Readability.Specs, false} 152 | 153 | # 154 | # Custom checks can be created using `mix credo.gen.check`. 155 | # 156 | ] 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin 2 | /deps 3 | /_build 4 | /.idea 5 | /bench/snapshots 6 | erl_crash.dump 7 | /cover 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: false 3 | elixir: 4 | - 1.5.1 5 | - 1.7.4 6 | - 1.8.0 7 | otp_release: 8 | - 19.0 9 | - 20.1 10 | - 21.2 11 | matrix: 12 | exclude: 13 | - elixir: 1.5.1 14 | otp_release: 21.2 15 | - elixir: 1.8.0 16 | otp_release: 18.3 17 | - elixir: 1.8.0 18 | otp_release: 19.0 19 | before_script: 20 | - mix local.hex --force 21 | - mix deps.get --only test 22 | script: mix test 23 | after_script: 24 | - MIX_ENV=docs mix deps.get 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquid [![Hex.pm](https://img.shields.io/hexpm/v/liquid.svg)](https://hex.pm/packages/liquid) [![Hex.pm](https://img.shields.io/hexpm/dt/liquid.svg)](https://hex.pm/packages/liquid) [![Build Status](https://travis-ci.org/bettyblocks/liquid-elixir.svg?branch=master)](https://travis-ci.org/bettyblocks/liquid-elixir) 2 | 3 | It's a templating library for Elixir. 4 | Continuation of the fluid liquid conversion. 5 | 6 | ## Usage 7 | 8 | Add the dependency to your mix file: 9 | 10 | ``` elixir 11 | # mix.exs 12 | defp deps do 13 | […, 14 | {:liquid, "~> 0.8.0"}] 15 | end 16 | ``` 17 | 18 | You can either start the application directly: 19 | 20 | `Liquid.start` 21 | 22 | Or start it with your application: 23 | 24 | ``` elixir 25 | # mix.exs 26 | def application do 27 | [mod: {MyApp, []}, 28 | applications: […, :liquid]] 29 | end 30 | ``` 31 | 32 | Compile a template from a string: 33 | 34 | `template = Liquid.Template.parse("{% assign hello='hello' %}{{ hello }}{{world}}")` 35 | 36 | Render the template with a keyword list representing the local variables: 37 | 38 | `{ :ok, rendered, _ } = Liquid.Template.render(template, %{"world" => "world"})` 39 | 40 | For registers you might want to use in custom tags you can assign them like this: 41 | 42 | `{ :ok, rendered, _ } = Liquid.Template.render(template, %{"world" => "world"}, registers: %{test: "hallo")` 43 | 44 | The tests should give a pretty good idea of the features implemented so far. 45 | 46 | ## Custom tags and filters 47 | 48 | You can add your own filters and tags/blocks inside your project: 49 | 50 | ``` elixir 51 | defmodule MyFilters do 52 | def meaning_of_life(_), do: 42 53 | def one(_), do: 1 54 | end 55 | 56 | defmodule ExampleTag do 57 | def parse(%Liquid.Tag{}=tag, %Liquid.Template{}=context) do 58 | {tag, context} 59 | end 60 | 61 | def render(output, tag, context) do 62 | number = tag.markup |> Integer.parse |> elem(0) 63 | {["#{number - 1}"] ++ output, context} 64 | end 65 | end 66 | 67 | defmodule ExampleBlock do 68 | def parse(b, p), do: { b, p } 69 | end 70 | ``` 71 | 72 | and than include them in your `config.exs` file 73 | 74 | ``` elixir 75 | # config.exs 76 | config :liquid, 77 | extra_filter_modules: [MyFilters], 78 | extra_tags: %{minus_one: {ExampleTag, Liquid.Tag}, 79 | my_block: {ExampleBlock, Liquid.Block}} 80 | ``` 81 | 82 | Another option is to set up the tag using: 83 | `Liquid.Registers.register("minus_one", MinusOneTag, Tag)` for tag 84 | `Liquid.Registers.register("my_block", ExampleBlock, Liquid.Block)` same for blocks; 85 | and for filters you should use 86 | `Liquid.Filters.add_filters(MyFilters)` 87 | 88 | #### Global Filters 89 | It's also possible to apply global filter to all rendered variables setting up the config: 90 | ``` elixir 91 | # config.exs 92 | config :liquid, 93 | global_filter: &MyFilter.counting_sheeps/1 94 | ``` 95 | or adding a `"global_filter"` value to context for `Liquid.Template.render` function: 96 | `Liquid.Template.render(tpl, %{global_filter: &MyFilter.counting_sheeps/1})` (you need to define filter function first) 97 | 98 | ## File systems 99 | You can also set up the desired default file system for your project using the `config.exs` file 100 | ``` elixir 101 | # config.exs 102 | config :liquid, 103 | file_system: {Liquid.LocalFileSystem, "/your/path"} 104 | ``` 105 | 106 | 107 | ## Context assignment 108 | 109 | `Liquid.Matcher` protocol is designed to deal with your custom data types you want to assign 110 | For example having the following struct: 111 | ``` elixir 112 | defmodule User do 113 | defstruct name: "John", age: 27, about: [] 114 | end 115 | ``` 116 | You can describe how to get the data from it: 117 | ``` elixir 118 | defimpl Liquid.Matcher, for: User do 119 | def match(current, ["info"|_]=parts) do 120 | "His name is: "<> current.name 121 | end 122 | end 123 | ``` 124 | And later you can use it in your code: 125 | ``` elixir 126 | iex> "{{ info }}" |> Liquid.Template.parse |> Liquid.Template.render(%User{}) |> elem(1) 127 | "His name is: John" 128 | ``` 129 | 130 | ## Missing Features 131 | 132 | Feel free to add a bug report or pull request if you feel that anything is missing. 133 | 134 | ### todo 135 | 136 | * Fix empty check on arrays 137 | -------------------------------------------------------------------------------- /bench/basic_bench.exs: -------------------------------------------------------------------------------- 1 | defmodule BasicBench do 2 | use Benchfella 3 | alias Liquid.{Template, Tag} 4 | 5 | @list Enum.to_list(1..1000) 6 | 7 | defmodule MyFilter do 8 | def meaning_of_life(_), do: 42 9 | end 10 | 11 | defmodule MyFilterTwo do 12 | def meaning_of_life(_), do: 40 13 | def plus_one(input) when is_binary(input) do 14 | input |> Integer.parse |> elem(0) |> plus_one 15 | end 16 | def plus_one(input) when is_number(input), do: input + 1 17 | def not_meaning_of_life(_), do: 2 18 | end 19 | 20 | defmodule MinusOneTag do 21 | def parse(%Tag{}=tag, %Template{}=context) do 22 | {tag, context} 23 | end 24 | 25 | def render(_input, tag, context) do 26 | number = tag.markup |> Integer.parse |> elem(0) 27 | {["#{number - 1}"], context} 28 | end 29 | end 30 | 31 | setup_all do 32 | Application.put_env(:liquid, :extra_filter_modules, [MyFilter, MyFilterTwo]) 33 | Liquid.start 34 | Liquid.Registers.register("minus_one", MinusOneTag, Tag) 35 | {:ok, nil} 36 | end 37 | 38 | bench "Loop list" do 39 | assigns = %{"array" => @list} 40 | markup = "{%for item in array %}{{item}}{%endfor%}" 41 | t = Template.parse(markup) 42 | { :ok, _rendered, _ } = Template.render(t, assigns) 43 | end 44 | 45 | bench "Loop custom filters and tags list" do 46 | assigns = %{"array" => @list} 47 | markup = "{%for item in array %}{%minus_one 3%}{{item | plus_one }}{%endfor%}" 48 | t = Template.parse(markup) 49 | { :ok, _rendered, _ } = Template.render(t, assigns) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/errors/errors.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.SyntaxError do 2 | @moduledoc """ 3 | Error module to hold wrong syntax states 4 | """ 5 | defexception message: "Liquid syntax error has occurred." 6 | end 7 | 8 | defmodule Liquid.FileSystemError do 9 | @moduledoc """ 10 | Error module to hold file system errors. 11 | """ 12 | defexception message: "Liquid error: Illegal template name" 13 | end 14 | -------------------------------------------------------------------------------- /lib/liquid.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid do 2 | use Application 3 | 4 | def start(_type, _args), do: start() 5 | 6 | def start do 7 | Liquid.Filters.add_filter_modules() 8 | Liquid.Supervisor.start_link() 9 | end 10 | 11 | def stop, do: {:ok, "stopped"} 12 | 13 | def filter_arguments, do: ~r/(?::|,)\s*(#{quoted_fragment()})/ 14 | def single_quote, do: "'" 15 | def double_quote, do: "\"" 16 | def quote_matcher, do: ~r/#{single_quote()}|#{double_quote()}/ 17 | 18 | def variable_start, do: "{{" 19 | def variable_end, do: "}}" 20 | def variable_incomplete_end, do: "\}\}?" 21 | 22 | def tag_start, do: "{%" 23 | def tag_end, do: "%}" 24 | 25 | def any_starting_tag, do: "(){{()|(){%()" 26 | 27 | def invalid_expression, 28 | do: ~r/^{%.*}}$|^{{.*%}$|^{%.*([^}%]}|[^}%])$|^{{.*([^}%]}|[^}%])$|(^{{|^{%)/ms 29 | 30 | def tokenizer, 31 | do: ~r/()#{tag_start()}.*?#{tag_end()}()|()#{variable_start()}.*?#{variable_end()}()/ 32 | 33 | def parser, 34 | do: 35 | ~r/#{tag_start()}\s*(?.*?)\s*#{tag_end()}|#{variable_start()}\s*(?.*?)\s*#{ 36 | variable_end() 37 | }/m 38 | 39 | def template_parser, do: ~r/#{partial_template_parser()}|#{any_starting_tag()}/ms 40 | 41 | def partial_template_parser, 42 | do: "()#{tag_start()}.*?#{tag_end()}()|()#{variable_start()}.*?#{variable_incomplete_end()}()" 43 | 44 | def quoted_string, do: "\"[^\"]*\"|'[^']*'" 45 | def quoted_fragment, do: "#{quoted_string()}|(?:[^\s,\|'\"]|#{quoted_string()})+" 46 | 47 | def tag_attributes, do: ~r/(\w+)\s*\:\s*(#{quoted_fragment()})/ 48 | def variable_parser, do: ~r/\[[^\]]+\]|[\w\-]+/ 49 | def filter_parser, do: ~r/(?:\||(?:\s*(?!(?:\|))(?:#{quoted_fragment()}|\S+)\s*)+)/ 50 | 51 | defmodule List do 52 | def even_elements([_, h | t]) do 53 | [h] ++ even_elements(t) 54 | end 55 | 56 | def even_elements([]), do: [] 57 | end 58 | 59 | defmodule Atomizer do 60 | def to_existing_atom(string) do 61 | try do 62 | String.to_existing_atom(string) 63 | rescue 64 | ArgumentError -> nil 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/liquid/appointer.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Appointer do 2 | @moduledoc "A module to assign context for `Liquid.Variable`" 3 | alias Liquid.{Matcher, Variable} 4 | 5 | @literals %{ 6 | "nil" => nil, 7 | "null" => nil, 8 | "" => nil, 9 | "true" => true, 10 | "false" => false, 11 | "blank" => :blank?, 12 | "empty" => :empty? 13 | } 14 | 15 | @integer ~r/^(-?\d+)$/ 16 | @float ~r/^(-?\d[\d\.]+)$/ 17 | @start_quoted_string ~r/^#{Liquid.quoted_string()}/ 18 | 19 | @doc "Assigns context for Variable and filters" 20 | def assign(%Variable{literal: literal, parts: [], filters: filters}, context) do 21 | {literal, filters |> assign_context(context.assigns)} 22 | end 23 | 24 | def assign( 25 | %Variable{literal: nil, parts: parts, filters: filters}, 26 | %{assigns: assigns} = context 27 | ) do 28 | {match(context, parts), filters |> assign_context(assigns)} 29 | end 30 | 31 | @doc "Verifies matches between Variable and filters, data types and parts" 32 | def match(%{assigns: assigns} = context, [key | _] = parts) when is_binary(key) do 33 | case assigns do 34 | %{^key => _value} -> match(assigns, parts) 35 | _ -> Matcher.match(context, parts) 36 | end 37 | end 38 | 39 | def match(current, []), do: current 40 | 41 | def match(current, [name | parts]) when is_binary(name) do 42 | current |> match(name) |> Matcher.match(parts) 43 | end 44 | 45 | def match(current, key) when is_binary(key), do: Map.get(current, key) 46 | 47 | @doc """ 48 | Makes `Variable.parts` or literals from the given markup 49 | """ 50 | @spec parse_name(String.t()) :: map() 51 | def parse_name(name) do 52 | value = 53 | cond do 54 | Map.has_key?(@literals, name) -> 55 | Map.get(@literals, name) 56 | 57 | Regex.match?(@integer, name) -> 58 | String.to_integer(name) 59 | 60 | Regex.match?(@float, name) -> 61 | String.to_float(name) 62 | 63 | Regex.match?(@start_quoted_string, name) -> 64 | Regex.replace(Liquid.quote_matcher(), name, "") 65 | 66 | true -> 67 | Liquid.variable_parser() |> Regex.scan(name) |> List.flatten() 68 | end 69 | 70 | if is_list(value), do: %{parts: value}, else: %{literal: value} 71 | end 72 | 73 | defp assign_context(filters, assigns) when assigns == %{}, do: filters 74 | defp assign_context([], _), do: [] 75 | 76 | defp assign_context([head | tail], assigns) do 77 | [name, args] = head 78 | 79 | args = 80 | for arg <- args do 81 | parsed = parse_name(arg) 82 | 83 | cond do 84 | Map.has_key?(parsed, :parts) -> 85 | assigns |> Matcher.match(parsed.parts) |> to_string() 86 | 87 | Map.has_key?(assigns, :__struct__) -> 88 | key = String.to_atom(arg) 89 | if Map.has_key?(assigns, key), do: to_string(assigns[key]), else: arg 90 | 91 | true -> 92 | if Map.has_key?(assigns, arg), do: to_string(assigns[arg]), else: arg 93 | end 94 | end 95 | 96 | [[name, args] | assign_context(tail, assigns)] 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/liquid/block.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Block do 2 | defstruct name: nil, 3 | markup: nil, 4 | condition: nil, 5 | parts: [], 6 | iterator: [], 7 | nodelist: [], 8 | elselist: [], 9 | blank: false, 10 | strict: true 11 | 12 | alias Liquid.Tag, as: Tag 13 | alias Liquid.Block, as: Block 14 | 15 | def create(markup) do 16 | destructure [name, rest], String.split(markup, " ", parts: 2) 17 | %Block{name: name |> String.to_atom(), markup: rest} 18 | end 19 | 20 | def split(nodes), do: split(nodes, [:else]) 21 | def split(%Block{nodelist: nodelist}, namelist), do: split(nodelist, namelist) 22 | 23 | def split(nodelist, namelist) when is_list(nodelist) do 24 | Enum.split_while(nodelist, fn x -> 25 | !(is_map(x) and x.__struct__ == Tag and Enum.member?(namelist, x.name)) 26 | end) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/liquid/condition.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Condition do 2 | defstruct left: nil, operator: nil, right: nil, child_operator: nil, child_condition: nil 3 | 4 | alias Liquid.Context, as: Context 5 | alias Liquid.Variable, as: Variable 6 | alias Liquid.Condition, as: Cond 7 | alias Liquid.Variable, as: Vars 8 | 9 | def create([h | t]) do 10 | head = create(h) 11 | create(head, t) 12 | end 13 | 14 | def create(<>) do 15 | left = Vars.create(left) 16 | %Cond{left: left} 17 | end 18 | 19 | def create({<>, operator, <>}) do 20 | create({left |> Vars.create(), operator, right |> Vars.create()}) 21 | end 22 | 23 | def create({%Variable{} = left, operator, <>}) do 24 | create({left, operator, right |> Vars.create()}) 25 | end 26 | 27 | def create({<>, operator, %Variable{} = right}) do 28 | create({left |> Vars.create(), operator, right}) 29 | end 30 | 31 | def create({%Variable{} = left, operator, %Variable{} = right}) do 32 | operator = String.to_atom(operator) 33 | %Cond{left: left, operator: operator, right: right} 34 | end 35 | 36 | def create(condition, []), do: condition 37 | 38 | def create(condition, [join | right]) when join == "and" or join == "or" do 39 | right = create(right) 40 | join = join |> String.trim() |> String.to_atom() 41 | join(join, condition, right) 42 | end 43 | 44 | def join(operator, condition, {_, _, _} = right), do: join(operator, condition, right |> create) 45 | 46 | def join(operator, condition, %Cond{child_condition: %Cond{} = right} = outer_right) do 47 | %{outer_right | child_condition: join(operator, condition, right), child_operator: operator} 48 | end 49 | 50 | def join(operator, condition, %Cond{} = right) do 51 | %{right | child_condition: condition, child_operator: operator} 52 | end 53 | 54 | def evaluate(%Cond{} = condition), do: evaluate(condition, %Context{}) 55 | 56 | def evaluate(%Cond{left: left, right: nil} = condition, %Context{} = context) do 57 | {current, context} = Vars.lookup(left, context) 58 | eval_child(!!current, condition.child_operator, condition.child_condition, context) 59 | end 60 | 61 | def evaluate( 62 | %Cond{left: left, right: right, operator: operator} = condition, 63 | %Context{} = context 64 | ) do 65 | {left, _} = Variable.lookup(left, context) 66 | {right, _} = Variable.lookup(right, context) 67 | current = eval_operator(left, operator, right) 68 | eval_child(current, condition.child_operator, condition.child_condition, context) 69 | end 70 | 71 | defp eval_child(current, nil, nil, _), do: current 72 | 73 | defp eval_child(current, :and, condition, context) do 74 | current and evaluate(condition, context) 75 | end 76 | 77 | defp eval_child(current, :or, condition, context) do 78 | current or evaluate(condition, context) 79 | end 80 | 81 | defp eval_operator(left, operator, right) 82 | when (is_nil(left) or is_nil(right)) and not (is_nil(left) and is_nil(right)) and 83 | operator in [:>=, :>, :<, :<=], 84 | do: false 85 | 86 | defp eval_operator([] = left, :==, :empty?), do: Enum.empty?(left) 87 | defp eval_operator([] = left, :<>, :empty?), do: eval_operator(left, :!==, :empty?) 88 | defp eval_operator([] = left, :!=, :empty?), do: !Enum.empty?(left) 89 | 90 | defp eval_operator(left, operator, right) do 91 | case operator do 92 | :== -> 93 | left == right 94 | 95 | :>= -> 96 | left >= right 97 | 98 | :> -> 99 | left > right 100 | 101 | :<= -> 102 | left <= right 103 | 104 | :< -> 105 | left < right 106 | 107 | :!= -> 108 | left != right 109 | 110 | :<> -> 111 | left != right 112 | 113 | :contains -> 114 | contains(left, right) 115 | 116 | _ -> 117 | raise Liquid.SyntaxError, 118 | message: "Unexpected character in '#{left} #{operator} #{right}'" 119 | end 120 | end 121 | 122 | defp contains(nil, _), do: false 123 | defp contains(_, nil), do: false 124 | 125 | defp contains(<>, <>), 126 | do: contains(left |> to_charlist, right |> to_charlist) 127 | 128 | defp contains(left, <>) when is_list(left), 129 | do: contains(left, right |> to_charlist) 130 | 131 | defp contains(<>, right) when is_list(right), 132 | do: contains(left |> to_charlist, right) 133 | 134 | defp contains(left, right) when is_list(left) and not is_list(right), 135 | do: contains(left, [right]) 136 | 137 | defp contains(left, right) when is_list(right) and is_list(left), 138 | do: :string.rstr(left, right) > 0 139 | end 140 | -------------------------------------------------------------------------------- /lib/liquid/context.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Context do 2 | defstruct assigns: %{}, 3 | offsets: %{}, 4 | registers: %{}, 5 | presets: %{}, 6 | blocks: [], 7 | extended: false, 8 | continue: false, 9 | break: false, 10 | template: nil, 11 | global_filter: nil, 12 | extra_tags: %{} 13 | 14 | def registers(context, key) do 15 | context.registers |> Map.get(key) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/liquid/expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Expression do 2 | alias Liquid.Variable 3 | alias Liquid.RangeLookup 4 | 5 | @literal_list [nil, "nil", "null", "", "true", "false", "blank", "empty"] 6 | @literals %{ 7 | nil => nil, 8 | "nil" => nil, 9 | "null" => nil, 10 | "" => nil, 11 | "true" => true, 12 | "false" => false, 13 | "blank" => :blank?, 14 | "empty" => :empty? 15 | } 16 | 17 | def parse(markup) when markup in @literal_list, do: @literals[markup] 18 | 19 | def parse(markup) do 20 | cond do 21 | # Single quoted strings 22 | Regex.match?(~r/\A'(.*)'\z/m, markup) -> 23 | [result] = Regex.run(~r/\A'(.*)'\z/m, markup, capture: :all_but_first) 24 | result 25 | 26 | # Double quoted strings 27 | Regex.match?(~r/\A"(.*)"\z/m, markup) -> 28 | [result] = Regex.run(~r/\A"(.*)"\z/m, markup, capture: :all_but_first) 29 | result 30 | 31 | # Integer and floats 32 | Regex.match?(~r/\A(-?\d+)\z/, markup) -> 33 | [result] = Regex.run(~r/\A(-?\d+)\z/, markup, capture: :all_but_first) 34 | String.to_integer(result) 35 | 36 | # Ranges 37 | Regex.match?(~r/\A\((\S+)\.\.(\S+)\)\z/, markup) -> 38 | [left_range, right_range] = 39 | Regex.run(~r/\A\((\S+)\.\.(\S+)\)\z/, markup, capture: :all_but_first) 40 | 41 | RangeLookup.parse(left_range, right_range) 42 | 43 | # Floats 44 | Regex.match?(~r/\A(-?\d[\d\.]+)\z/, markup) -> 45 | [result] = Regex.run(~r/\A(-?\d[\d\.]+)\z/, markup, capture: :all_but_first) 46 | result 47 | 48 | true -> 49 | Variable.create(markup) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/liquid/file_system.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.BlankFileSystem do 2 | def read_template_file(_root, _name, _context) do 3 | {:error, "This liquid context does not allow includes."} 4 | end 5 | end 6 | 7 | defmodule Liquid.LocalFileSystem do 8 | def read_template_file(_root, _name, _context) do 9 | {:ok, ""} 10 | end 11 | 12 | def full_path(root, template_path) do 13 | full_path = 14 | if Regex.match?(~r/\//, template_path) do 15 | root 16 | |> Path.join(template_path |> Path.dirname()) 17 | |> Path.join("_#{template_path |> Path.basename()}.liquid") 18 | else 19 | root |> Path.join("_#{template_path}.liquid") 20 | end 21 | 22 | cond do 23 | !Regex.match?(~r/^[^.\/][a-zA-Z0-9_\/]+$/, template_path) -> 24 | {:error, "Illegal template name '#{template_path}'"} 25 | 26 | !Regex.match?(~r/^#{Path.expand(root)}/, Path.expand(full_path)) -> 27 | {:error, "Illegal template path '#{Path.expand(full_path)}'"} 28 | 29 | true -> 30 | {:ok, full_path} 31 | end 32 | end 33 | end 34 | 35 | defmodule Liquid.FileSystem do 36 | @moduledoc """ 37 | Allows to set up the file system and read the template file from it 38 | """ 39 | 40 | @doc """ 41 | Get full file system path 42 | """ 43 | def full_path(path) do 44 | case lookup() do 45 | nil -> {:error, "No file system defined"} 46 | {mod, root} -> mod.full_path(root, path) 47 | end 48 | end 49 | 50 | def read_template_file(path, options \\ []) do 51 | case lookup() do 52 | nil -> {:error, "No file system defined"} 53 | {mod, root} -> mod.read_template_file(root, path, options) 54 | end 55 | end 56 | 57 | def register(module, path \\ "") do 58 | Application.put_env(:liquid, :file_system, {module, path}) 59 | end 60 | 61 | def lookup do 62 | Application.get_env(:liquid, :file_system) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/liquid/include.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Include do 2 | alias Liquid.Tag, as: Tag 3 | alias Liquid.Context, as: Context 4 | alias Liquid.Template, as: Template 5 | alias Liquid.Variable, as: Variable 6 | alias Liquid.FileSystem, as: FileSystem 7 | 8 | def syntax, 9 | do: ~r/(#{Liquid.quoted_fragment()}+)(\s+(?:with|for)\s+(#{Liquid.quoted_fragment()}+))?/ 10 | 11 | def parse(%Tag{markup: markup} = tag, %Template{} = template) do 12 | [parts | _] = syntax() |> Regex.scan(markup) 13 | tag = parse_tag(tag, parts) 14 | attributes = parse_attributes(markup) 15 | {%{tag | attributes: attributes}, template} 16 | end 17 | 18 | defp parse_tag(%Tag{} = tag, parts) do 19 | case parts do 20 | [_, name] -> 21 | %{tag | parts: [name: name |> Variable.create()]} 22 | 23 | [_, name, " with " <> _, v] -> 24 | %{tag | parts: [name: name |> Variable.create(), variable: v |> Variable.create()]} 25 | 26 | [_, name, " for " <> _, v] -> 27 | %{tag | parts: [name: name |> Variable.create(), foreach: v |> Variable.create()]} 28 | end 29 | end 30 | 31 | defp parse_attributes(markup) do 32 | Liquid.tag_attributes() 33 | |> Regex.scan(markup) 34 | |> Enum.reduce(%{}, fn [_, key, val], coll -> 35 | Map.put(coll, key, val |> Variable.create()) 36 | end) 37 | end 38 | 39 | def render(output, %Tag{parts: parts} = tag, %Context{} = context) do 40 | {file_system, root} = context |> Context.registers(:file_system) || FileSystem.lookup() 41 | {name, context} = parts[:name] |> Variable.lookup(context) 42 | {:ok, source} = file_system.read_template_file(root, name, context) 43 | presets = build_presets(tag, context) 44 | t = Template.parse(source, presets) 45 | t = %{t | blocks: context.template.blocks ++ t.blocks} 46 | 47 | cond do 48 | !is_nil(parts[:variable]) -> 49 | {item, context} = Variable.lookup(parts[:variable], context) 50 | render_item(output, name, item, t, context) 51 | 52 | !is_nil(parts[:foreach]) -> 53 | {items, context} = Variable.lookup(parts[:foreach], context) 54 | render_list(output, name, items, t, context) 55 | 56 | true -> 57 | render_item(output, name, nil, t, context) 58 | end 59 | end 60 | 61 | defp build_presets(%Tag{} = tag, context) do 62 | tag.attributes 63 | |> Enum.reduce(%{}, fn {key, value}, coll -> 64 | {value, _} = Variable.lookup(value, context) 65 | Map.put(coll, key, value) 66 | end) 67 | end 68 | 69 | defp render_list(output, _, [], _, context) do 70 | {output, context} 71 | end 72 | 73 | defp render_list(output, key, [item | rest], template, %Context{} = context) do 74 | {output, context} = render_item(output, key, item, template, context) 75 | render_list(output, key, rest, template, context) 76 | end 77 | 78 | defp render_item(output, _key, nil, template, %Context{} = context) do 79 | {:ok, rendered, _} = Template.render(template, context) 80 | {[rendered] ++ output, context} 81 | end 82 | 83 | defp render_item(output, key, item, template, %Context{} = context) do 84 | assigns = context.assigns |> Map.merge(%{key => item}) 85 | {:ok, rendered, _} = Template.render(template, %{context | assigns: assigns}) 86 | {[rendered] ++ output, context} 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/liquid/parse.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Parse do 2 | alias Liquid.Template 3 | alias Liquid.Variable 4 | alias Liquid.Registers 5 | alias Liquid.Block 6 | 7 | def tokenize(<>) do 8 | Liquid.template_parser() 9 | |> Regex.split(string, on: :all_but_first, trim: true) 10 | |> List.flatten() 11 | |> Enum.filter(&(&1 != "")) 12 | end 13 | 14 | def parse("", %Template{} = template) do 15 | %{template | root: %Liquid.Block{name: :document}} 16 | end 17 | 18 | def parse(<>, %Template{} = template) do 19 | tokens = string |> tokenize 20 | name = tokens |> hd 21 | tag_name = parse_tag_name(name) 22 | tokens = parse_tokens(string, tag_name) || tokens 23 | {root, template} = parse(%Liquid.Block{name: :document}, tokens, [], template) 24 | %{template | root: root} 25 | end 26 | 27 | def parse(%Block{name: :document} = block, [], accum, %Template{} = template) do 28 | unless nodelist_invalid?(block, accum), do: {%{block | nodelist: accum}, template} 29 | end 30 | 31 | def parse(%Block{name: :comment} = block, [h | t], accum, %Template{} = template) do 32 | cond do 33 | Regex.match?(~r/{%\s*endcomment\s*%}/, h) -> 34 | {%{block | nodelist: accum}, t, template} 35 | 36 | Regex.match?(~r/{%\send.*?\s*$}/, h) -> 37 | raise "Unmatched block close: #{h}" 38 | 39 | true -> 40 | {result, rest, template} = 41 | try do 42 | parse_node(h, t, template) 43 | rescue 44 | # Ignore undefined tags inside comments 45 | RuntimeError -> 46 | {h, t, template} 47 | end 48 | 49 | parse(block, rest, accum ++ [result], template) 50 | end 51 | end 52 | 53 | def parse(%Block{name: name}, [], _, _) do 54 | raise "No matching end for block {% #{to_string(name)} %}" 55 | end 56 | 57 | def parse(%Block{name: name} = block, [h | t], accum, %Template{} = template) do 58 | endblock = "end" <> to_string(name) 59 | 60 | cond do 61 | Regex.match?(~r/{%\s*#{endblock}\s*%}/, h) -> 62 | unless nodelist_invalid?(block, accum), do: {%{block | nodelist: accum}, t, template} 63 | 64 | Regex.match?(~r/{%\send.*?\s*$}/, h) -> 65 | raise "Unmatched block close: #{h}" 66 | 67 | true -> 68 | {result, rest, template} = parse_node(h, t, template) 69 | parse(block, rest, accum ++ [result], template) 70 | end 71 | end 72 | 73 | defp invalid_expression?(expression) when is_binary(expression) do 74 | Regex.match?(Liquid.invalid_expression(), expression) 75 | end 76 | 77 | defp invalid_expression?(_), do: false 78 | 79 | defp nodelist_invalid?(block, nodelist) do 80 | case block.strict do 81 | true -> 82 | if Enum.any?(nodelist, &invalid_expression?(&1)) do 83 | raise Liquid.SyntaxError, 84 | message: "no match delimiters in #{block.name}: #{block.markup}" 85 | end 86 | 87 | false -> 88 | false 89 | end 90 | end 91 | 92 | defp parse_tokens(<>, tag_name) do 93 | case Registers.lookup(tag_name) do 94 | {mod, Liquid.Block} -> 95 | try do 96 | mod.tokenize(string) 97 | rescue 98 | UndefinedFunctionError -> nil 99 | end 100 | 101 | _ -> 102 | nil 103 | end 104 | end 105 | 106 | defp parse_tag_name(name) do 107 | case Regex.named_captures(Liquid.parser(), name) do 108 | %{"tag" => tag_name, "variable" => _} -> tag_name 109 | _ -> nil 110 | end 111 | end 112 | 113 | defp parse_node(<>, rest, %Template{} = template) do 114 | case Regex.named_captures(Liquid.parser(), name) do 115 | %{"tag" => "", "variable" => markup} when is_binary(markup) -> 116 | {Variable.create(markup), rest, template} 117 | 118 | %{"tag" => markup, "variable" => ""} when is_binary(markup) -> 119 | parse_markup(markup, rest, template) 120 | 121 | nil -> 122 | {name, rest, template} 123 | end 124 | end 125 | 126 | defp parse_markup(markup, rest, template) do 127 | name = markup |> String.split(" ") |> hd 128 | 129 | case Registers.lookup(name) do 130 | {mod, Liquid.Block} -> 131 | parse_block(mod, markup, rest, template) 132 | 133 | {mod, Liquid.Tag} -> 134 | tag = Liquid.Tag.create(markup) 135 | {tag, template} = mod.parse(tag, template) 136 | {tag, rest, template} 137 | 138 | nil -> 139 | raise "unregistered tag: #{name}" 140 | end 141 | end 142 | 143 | defp parse_block(mod, markup, rest, template) do 144 | block = Liquid.Block.create(markup) 145 | 146 | {block, rest, template} = 147 | try do 148 | mod.parse(block, rest, [], template) 149 | rescue 150 | UndefinedFunctionError -> parse(block, rest, [], template) 151 | end 152 | 153 | {block, template} = mod.parse(block, template) 154 | {block, rest, template} 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/liquid/range_lookup.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.RangeLookup do 2 | defstruct range_start: 0, range_end: 0 3 | alias Liquid.Expression 4 | alias Liquid.RangeLookup 5 | alias Liquid.Variable 6 | alias Liquid.Context 7 | 8 | def parse( 9 | %RangeLookup{range_start: %Variable{} = range_start, range_end: %Variable{} = range_end}, 10 | %Context{} = context 11 | ) do 12 | {rendered_left, _} = Variable.lookup(range_start, context) 13 | {rendered_right, _} = Variable.lookup(range_end, context) 14 | left = valid_range_value(rendered_left) 15 | right = valid_range_value(rendered_right, left) 16 | 17 | Enum.to_list(left..right) 18 | end 19 | 20 | def parse( 21 | %RangeLookup{range_start: range_start, range_end: %Variable{} = range_end}, 22 | %Context{} = context 23 | ) do 24 | {rendered_right, _} = Variable.lookup(range_end, context) 25 | right = valid_range_value(rendered_right, range_start) 26 | 27 | Enum.to_list(range_start..right) 28 | end 29 | 30 | def parse( 31 | %RangeLookup{range_start: %Variable{} = range_start, range_end: range_end}, 32 | %Context{} = context 33 | ) do 34 | {rendered_left, _} = Variable.lookup(range_start, context) 35 | left = valid_range_value(rendered_left) 36 | 37 | Enum.to_list(left..range_end) 38 | end 39 | 40 | def parse(left, right) do 41 | start_value = Expression.parse(left) 42 | end_value = Expression.parse(right) 43 | 44 | build_range(start_value, end_value) 45 | end 46 | 47 | defp valid_range_value(value, fallback \\ 0) 48 | 49 | defp valid_range_value(value, fallback) when is_binary(value) do 50 | if is_binary(value) do 51 | case Integer.parse(value) do 52 | :error -> fallback 53 | {value, _} -> value 54 | end 55 | end 56 | end 57 | 58 | defp valid_range_value(value, _), do: value 59 | 60 | defp build_range(left, right) when is_integer(left) and is_integer(right) do 61 | Enum.to_list(left..right) 62 | end 63 | 64 | defp build_range(left, right) when is_map(left) or is_map(right) do 65 | %RangeLookup{range_start: left, range_end: right} 66 | end 67 | 68 | defp build_range(left, right) do 69 | left = left |> to_string |> String.to_integer() 70 | right = right |> to_string |> String.to_integer() 71 | 72 | Enum.to_list(left..right) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/liquid/registers.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Registers do 2 | @default_tags %{ 3 | continue: {Liquid.Continue, Liquid.Tag}, 4 | comment: {Liquid.Comment, Liquid.Block}, 5 | include: {Liquid.Include, Liquid.Tag}, 6 | assign: {Liquid.Assign, Liquid.Tag}, 7 | block: {Liquid.Inherit, Liquid.Block}, 8 | break: {Liquid.Break, Liquid.Tag}, 9 | elsif: {Liquid.ElseIf, Liquid.Tag}, 10 | else: {Liquid.Else, Liquid.Tag}, 11 | case: {Liquid.Case, Liquid.Block}, 12 | when: {Liquid.When, Liquid.Tag}, 13 | for: {Liquid.ForElse, Liquid.Block}, 14 | tablerow: {Liquid.TableRow, Liquid.Block}, 15 | ifchanged: {Liquid.IfChanged, Liquid.Block}, 16 | if: {Liquid.IfElse, Liquid.Block}, 17 | unless: {Liquid.Unless, Liquid.Block}, 18 | raw: {Liquid.Raw, Liquid.Block}, 19 | increment: {Liquid.Increment, Liquid.Tag}, 20 | decrement: {Liquid.Decrement, Liquid.Tag}, 21 | cycle: {Liquid.Cycle, Liquid.Tag}, 22 | capture: {Liquid.Capture, Liquid.Block} 23 | } 24 | 25 | def clear do 26 | Application.put_env(:liquid, :extra_tags, %{}) 27 | end 28 | 29 | def lookup(name) when is_binary(name) do 30 | try do 31 | name 32 | |> String.to_existing_atom() 33 | |> lookup() 34 | rescue 35 | ArgumentError -> nil 36 | end 37 | end 38 | 39 | def lookup(name) when is_atom(name) do 40 | custom_tag = 41 | case Application.get_env(:liquid, :extra_tags) do 42 | %{^name => value} -> value 43 | _ -> nil 44 | end 45 | 46 | case {name, Map.get(@default_tags, name), custom_tag} do 47 | {nil, _, _} -> nil 48 | {_, nil, nil} -> nil 49 | {_, nil, custom_tag} -> custom_tag 50 | {_, tag, _} -> tag 51 | end 52 | end 53 | 54 | def lookup(_), do: nil 55 | 56 | def lookup(name, context) when is_binary(name) do 57 | name |> String.to_atom() |> lookup(context) 58 | end 59 | 60 | def lookup(name, %{extra_tags: extra_tags}) do 61 | custom_tag = Map.get(extra_tags, name) 62 | 63 | case {name, Map.get(@default_tags, name), custom_tag} do 64 | {nil, _, _} -> nil 65 | {_, nil, nil} -> nil 66 | {_, nil, custom_tag} -> custom_tag 67 | {_, tag, _} -> tag 68 | end 69 | end 70 | 71 | def lookup(_, _), do: nil 72 | 73 | def register(name, module, type) do 74 | custom_tags = Application.get_env(:liquid, :extra_tags) || %{} 75 | 76 | custom_tags = 77 | %{(name |> String.to_atom()) => {module, type}} 78 | |> Map.merge(custom_tags) 79 | 80 | Application.put_env(:liquid, :extra_tags, custom_tags) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/liquid/render.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Render do 2 | alias Liquid.Variable 3 | alias Liquid.Template 4 | alias Liquid.Registers 5 | alias Liquid.Context 6 | alias Liquid.Block 7 | alias Liquid.Tag 8 | 9 | def render(%Template{root: root}, %Context{} = context) do 10 | {output, context} = render([], root, context) 11 | {:ok, output |> to_text, context} 12 | end 13 | 14 | def render(output, [], %Context{} = context) do 15 | {output, context} 16 | end 17 | 18 | def render(output, [h | t], %Context{} = context) do 19 | {output, context} = render(output, h, context) 20 | 21 | case context do 22 | %Context{extended: false, break: false, continue: false} -> render(output, t, context) 23 | _ -> render(output, [], context) 24 | end 25 | end 26 | 27 | def render(output, text, %Context{} = context) when is_binary(text) do 28 | {[text | output], context} 29 | end 30 | 31 | def render(output, %Variable{} = variable, %Context{} = context) do 32 | {rendered, context} = Variable.lookup(variable, context) 33 | {[join_list(rendered) | output], context} 34 | end 35 | 36 | def render(output, %Tag{name: name} = tag, %Context{} = context) do 37 | {mod, Tag} = Registers.lookup(name) 38 | mod.render(output, tag, context) 39 | end 40 | 41 | def render(output, %Block{name: name} = block, %Context{} = context) do 42 | case Registers.lookup(name) do 43 | {mod, Block} -> mod.render(output, block, context) 44 | nil -> render(output, block.nodelist, context) 45 | end 46 | end 47 | 48 | def to_text(list), do: list |> List.flatten() |> Enum.reverse() |> Enum.join() 49 | 50 | defp join_list(input) when is_list(input), do: input |> List.flatten() |> Enum.join() 51 | 52 | defp join_list(input), do: input 53 | end 54 | -------------------------------------------------------------------------------- /lib/liquid/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Supervisor do 2 | @moduledoc """ 3 | Supervisor for Liquid processes (currently empty) 4 | """ 5 | use Supervisor 6 | 7 | @doc """ 8 | Starts the liquid supervisor 9 | """ 10 | def start_link do 11 | Supervisor.start_link(__MODULE__, :ok) 12 | end 13 | 14 | @doc """ 15 | Actual supervisor init with no child processes to supervise yet 16 | """ 17 | def init(:ok) do 18 | children = [] 19 | supervise(children, strategy: :one_for_one) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/liquid/tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Tag do 2 | defstruct name: nil, markup: nil, parts: [], attributes: [], blank: false 3 | 4 | def create(markup) do 5 | destructure [name, rest], String.split(markup, " ", parts: 2) 6 | %Liquid.Tag{name: name |> String.to_atom(), markup: rest} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/liquid/tags/assign.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Assign do 2 | alias Liquid.Variable 3 | alias Liquid.Tag 4 | alias Liquid.Context 5 | 6 | def syntax, do: ~r/([\w\-]+)\s*=\s*(.*)\s*/ 7 | 8 | def parse(%Tag{} = tag, %Liquid.Template{} = template), do: {%{tag | blank: true}, template} 9 | 10 | def render(output, %Tag{markup: markup}, %Context{} = context) do 11 | [[_, to, from]] = syntax() |> Regex.scan(markup) 12 | 13 | {from_value, context} = 14 | from 15 | |> Variable.create() 16 | |> Variable.lookup(context) 17 | 18 | result_assign = context.assigns |> Map.put(to, from_value) 19 | context = %{context | assigns: result_assign} 20 | {output, context} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/liquid/tags/capture.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Capture do 2 | alias Liquid.Block 3 | alias Liquid.Context 4 | alias Liquid.Template 5 | 6 | def parse(%Block{} = block, %Template{} = template) do 7 | {%{block | blank: true}, template} 8 | end 9 | 10 | def render(output, %Block{markup: markup, nodelist: content}, %Context{} = context) do 11 | variable_name = Liquid.variable_parser() |> Regex.run(markup) |> hd 12 | {block_output, context} = Liquid.Render.render([], content, context) 13 | 14 | result_assign = 15 | context.assigns |> Map.put(variable_name, block_output |> Liquid.Render.to_text()) 16 | 17 | context = %{context | assigns: result_assign} 18 | {output, context} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/liquid/tags/case.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Case do 2 | alias Liquid.Tag 3 | alias Liquid.Block 4 | alias Liquid.Template 5 | alias Liquid.Variable 6 | alias Liquid.Condition 7 | 8 | def syntax, do: ~r/(#{Liquid.quoted_fragment()})/ 9 | 10 | def when_syntax, 11 | do: ~r/(#{Liquid.quoted_fragment()})(?:(?:\s+or\s+|\s*\,\s*)(#{Liquid.quoted_fragment()}.*))?/ 12 | 13 | def parse(%Block{markup: markup} = b, %Template{} = t) do 14 | [[_, name]] = syntax() |> Regex.scan(markup) 15 | {split(name |> Variable.create(), b.nodelist), t} 16 | end 17 | 18 | defp split(%Variable{}, []), do: [] 19 | defp split(%Variable{} = v, [h | t]) when is_binary(h), do: split(v, t) 20 | defp split(%Variable{} = _, [%Liquid.Tag{name: :else} | t]), do: t 21 | 22 | defp split(%Variable{} = v, [%Liquid.Tag{name: :when, markup: markup} | t]) do 23 | {nodelist, t} = Block.split(t, [:when, :else]) 24 | condition = parse_condition(v, markup) 25 | %Block{name: :if, nodelist: nodelist, condition: condition, elselist: split(v, t)} 26 | end 27 | 28 | defp parse_condition(%Variable{} = v, <>) do 29 | {h, t} = parse_when(markup) 30 | parse_condition(v, Condition.create({v, "==", h}), t) 31 | end 32 | 33 | defp parse_condition(%Variable{} = _, %Condition{} = condition, []), do: condition 34 | 35 | defp parse_condition(%Variable{} = v, %Condition{} = condition, [<>]) do 36 | {h, t} = parse_when(markup) 37 | parse_condition(v, Condition.join(:or, condition, {v, "==", h}), t) 38 | end 39 | 40 | defp parse_when(markup) do 41 | [[_, h | t] | m] = when_syntax() |> Regex.scan(markup) 42 | m = m |> List.flatten() |> Liquid.List.even_elements() 43 | t = [t | m] |> Enum.join(" ") 44 | t = if t == "", do: [], else: [t] 45 | {h, t} 46 | end 47 | end 48 | 49 | defmodule Liquid.When do 50 | alias Liquid.Tag, as: Tag 51 | alias Liquid.Template, as: Template 52 | 53 | def parse(%Tag{} = tag, %Template{} = t), do: {tag, t} 54 | end 55 | -------------------------------------------------------------------------------- /lib/liquid/tags/comment.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Comment do 2 | def parse(%Liquid.Block{} = block, %Liquid.Template{} = template), 3 | do: {%{block | blank: true, strict: false}, template} 4 | 5 | def render(output, %Liquid.Block{}, context), do: {output, context} 6 | end 7 | -------------------------------------------------------------------------------- /lib/liquid/tags/cycle.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Cycle do 2 | @moduledoc """ 3 | Implementation of `cycle` tag. Can be named or anonymous, rotates through pre-set values 4 | Cycle is usually used within a loop to alternate between values, like colors or DOM classes. 5 | """ 6 | alias Liquid.{Tag, Template, Context, Variable} 7 | 8 | @colon_parser ~r/\:(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/ 9 | # @except_colon_parser ~r/(?:[^:"']|"[^"]*"|'[^']*')+/ 10 | 11 | @doc """ 12 | Sets up the cycle name and variables to cycle through 13 | """ 14 | def parse(%Tag{markup: markup} = tag, %Template{} = template) do 15 | {name, values} = markup |> get_name_and_values 16 | tag = %{tag | parts: [name | values]} 17 | {tag, template} 18 | end 19 | 20 | @doc """ 21 | Returns a corresponding cycle value and increments the cycle counter 22 | """ 23 | def render(output, %Tag{parts: [name | values]}, %Context{} = context) do 24 | {name, context} = Variable.lookup(%Variable{parts: [], literal: name}, context) 25 | name = to_string(name) <> "_liquid_cycle" 26 | {rendered, context} = Variable.lookup(%Variable{parts: [name], literal: nil}, context) 27 | index = rendered || 0 28 | {value, context} = values |> Enum.fetch!(index) |> get_value_from_context(context) 29 | new_index = rem(index + 1, Enum.count(values)) 30 | result_assign = Map.put(context.assigns, name, new_index) 31 | {[value | output], %{context | assigns: result_assign}} 32 | end 33 | 34 | defp get_value_from_context(name, context) do 35 | custom_value = Liquid.Appointer.parse_name(name) 36 | 37 | parsed = 38 | if custom_value |> Map.has_key?(:parts), 39 | do: List.first(custom_value.parts), 40 | else: custom_value.literal 41 | 42 | variable = %Variable{parts: [], literal: parsed} 43 | Variable.lookup(variable, context) 44 | end 45 | 46 | defp get_name_and_values(markup) do 47 | [name | values] = markup |> String.split(@colon_parser, parts: 2, trim: true) 48 | values = if values == [], do: [name], else: values 49 | values = values |> hd |> String.split(",", trim: true) |> Enum.map(&String.trim(&1)) 50 | {name, values} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/liquid/tags/decrement.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Decrement do 2 | alias Liquid.Tag 3 | alias Liquid.Template 4 | alias Liquid.Context 5 | alias Liquid.Variable 6 | 7 | def parse(%Tag{} = tag, %Template{} = template) do 8 | {tag, template} 9 | end 10 | 11 | def render(output, %Tag{markup: markup}, %Context{} = context) do 12 | variable = Variable.create(markup) 13 | {rendered, context} = Variable.lookup(variable, context) 14 | value = rendered || 0 15 | result_assign = context.assigns |> Map.put(markup, value - 1) 16 | context = %{context | assigns: result_assign} 17 | {[value - 1] ++ output, context} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/liquid/tags/for_else.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.ForElse do 2 | @moduledoc """ 3 | Like in Shopify's liquid: "For" iterates over an array or collection. 4 | Several useful variables are available to you within the loop. 5 | 6 | == Basic usage: 7 | {% for item in collection %} 8 | {{ forloop.index }}: {{ item.name }} 9 | {% endfor %} 10 | 11 | == Advanced usage: 12 | {% for item in collection %} 13 |
14 | Item {{ forloop.index }}: {{ item.name }} 15 |
16 | {% else %} 17 | There is nothing in the collection. 18 | {% endfor %} 19 | 20 | You can also define a limit and offset much like SQL. Remember 21 | that offset starts at 0 for the first item. 22 | 23 | {% for item in collection limit:5 offset:10 %} 24 | {{ item.name }} 25 | {% end %} 26 | 27 | To reverse the for loop simply use {% for item in collection reversed %} 28 | 29 | == Available variables: 30 | 31 | forloop.name:: 'item-collection' 32 | forloop.length:: Length of the loop 33 | forloop.index:: The current item's position in the collection; 34 | forloop.index starts at 1. 35 | This is helpful for non-programmers who start believe 36 | the first item in an array is 1, not 0. 37 | forloop.index0:: The current item's position in the collection 38 | where the first item is 0 39 | forloop.rindex:: Number of items remaining in the loop 40 | (length - index) where 1 is the last item. 41 | forloop.rindex0:: Number of items remaining in the loop 42 | where 0 is the last item. 43 | forloop.first:: Returns true if the item is the first item. 44 | forloop.last:: Returns true if the item is the last item. 45 | forloop.parentloop:: Provides access to the parent loop, if present. 46 | 47 | """ 48 | alias Liquid.Render 49 | alias Liquid.Block 50 | alias Liquid.Variable 51 | alias Liquid.Context 52 | alias Liquid.Expression 53 | alias Liquid.RangeLookup 54 | 55 | defmodule Iterator do 56 | defstruct name: nil, 57 | collection: nil, 58 | item: nil, 59 | reversed: false, 60 | limit: nil, 61 | offset: nil, 62 | forloop: %{} 63 | end 64 | 65 | def syntax, do: ~r/(\w+)\s+in\s+(#{Liquid.quoted_fragment()}+)\s*(reversed)?/ 66 | 67 | def parse(%Block{nodelist: nodelist} = block, %Liquid.Template{} = t) do 68 | block = %{block | iterator: parse_iterator(block)} 69 | 70 | case Block.split(block) do 71 | {true_block, [_, false_block]} -> 72 | is_blank = Blank.blank?([true_block | false_block]) 73 | {%{block | nodelist: true_block, elselist: false_block, blank: is_blank}, t} 74 | 75 | {_, []} -> 76 | is_blank = Blank.blank?(nodelist) 77 | {%{block | blank: is_blank}, t} 78 | end 79 | end 80 | 81 | defp parse_iterator(%Block{markup: markup}) do 82 | [[_, item | [orig_collection | reversed]]] = Regex.scan(syntax(), markup) 83 | collection = Expression.parse(orig_collection) 84 | reversed = !(reversed |> List.first() |> is_nil) 85 | attributes = Liquid.tag_attributes() |> Regex.scan(markup) 86 | limit = attributes |> parse_attribute("limit") |> Variable.create() 87 | offset = attributes |> parse_attribute("offset", "0") |> Variable.create() 88 | 89 | %Iterator{ 90 | name: orig_collection, 91 | item: item, 92 | collection: collection, 93 | limit: limit, 94 | offset: offset, 95 | reversed: reversed 96 | } 97 | end 98 | 99 | defp parse_attribute(attributes, name, default \\ "nil") do 100 | attributes 101 | |> Enum.reduce(default, fn x, ret -> 102 | case x do 103 | [_, ^name, attribute] when is_binary(attribute) -> attribute 104 | _ -> ret 105 | end 106 | end) 107 | end 108 | 109 | def render(output, %Block{iterator: it} = block, %Context{} = context) do 110 | {list, context} = parse_collection(it.collection, context) 111 | list = if is_binary(list) and list != "", do: [list], else: list 112 | 113 | if is_list(list) and !is_empty_list(list) do 114 | list = if it.reversed, do: Enum.reverse(list), else: list 115 | {limit, context} = lookup_limit(it, context) 116 | {offset, context} = lookup_offset(it, context) 117 | each(output, [make_ref(), limit, offset], list, block, context) 118 | else 119 | Render.render(output, block.elselist, context) 120 | end 121 | end 122 | 123 | defp is_empty_list([]), do: true 124 | defp is_empty_list(value) when is_list(value), do: false 125 | defp is_empty_list(_value), do: false 126 | 127 | defp parse_collection(list, context) when is_list(list), do: {list, context} 128 | 129 | defp parse_collection(%Variable{} = variable, context) do 130 | Variable.lookup(variable, context) 131 | end 132 | 133 | defp parse_collection(%RangeLookup{} = range, context) do 134 | {RangeLookup.parse(range, context), context} 135 | end 136 | 137 | def each(output, _, [], %Block{} = block, %Context{} = context), 138 | do: {output, remember_limit(block, context)} 139 | 140 | def each(output, [prev, limit, offset], [h | t] = list, %Block{} = block, %Context{} = context) do 141 | forloop = next_forloop(block.iterator, list) 142 | block = %{block | iterator: %{block.iterator | forloop: forloop}} 143 | 144 | assigns = 145 | context.assigns 146 | |> Map.put("forloop", forloop) 147 | |> Map.put(block.iterator.item, h) 148 | 149 | registers = context.registers |> Map.put("changed", {prev, h}) 150 | 151 | {output, block_context} = 152 | render_content(output, block, %{context | assigns: assigns, registers: registers}, [ 153 | limit, 154 | offset 155 | ]) 156 | 157 | t = if block_context.break == true, do: [], else: t 158 | 159 | each(output, [h, limit, offset], t, block, %{ 160 | context 161 | | assigns: block_context.assigns, 162 | registers: block_context.registers 163 | }) 164 | end 165 | 166 | defp render_content( 167 | output, 168 | %Block{iterator: %{forloop: %{"index" => index}}, nodelist: nodelist, blank: blank}, 169 | context, 170 | [limit, offset] 171 | ) do 172 | case {should_render?(limit, offset, index), blank} do 173 | {true, true} -> 174 | {_, new_context} = Render.render([], nodelist, context) 175 | {output, new_context} 176 | 177 | {true, _} -> 178 | Render.render(output, nodelist, context) 179 | 180 | _ -> 181 | {output, context} 182 | end 183 | end 184 | 185 | defp remember_limit(%Block{iterator: %{name: name} = it}, %{offsets: offsets} = context) do 186 | {rendered, context} = lookup_limit(it, context) 187 | limit = rendered || 0 188 | remembered = Map.get(offsets, name, 0) 189 | %{context | offsets: offsets |> Map.put(name, remembered + limit)} 190 | end 191 | 192 | defp should_render?(_limit, offset, index) when index <= offset, do: false 193 | defp should_render?(nil, _, _), do: true 194 | defp should_render?(limit, offset, index) when index > limit + offset, do: false 195 | defp should_render?(_limit, _offset, _index), do: true 196 | 197 | defp lookup_limit(%Iterator{limit: limit}, %Context{} = context), 198 | do: Variable.lookup(limit, context) 199 | 200 | defp lookup_offset( 201 | %Iterator{offset: %Variable{name: "continue"}, name: name}, 202 | %Context{offsets: offsets} = context 203 | ) do 204 | {Map.get(offsets, name, 0), context} 205 | end 206 | 207 | defp lookup_offset(%Iterator{offset: offset}, %Context{} = context), 208 | do: Variable.lookup(offset, context) 209 | 210 | defp next_forloop(%Iterator{forloop: loop, item: item, name: name}, count) 211 | when map_size(loop) < 1 do 212 | count = Enum.count(count) 213 | 214 | %{ 215 | "name" => item <> "-" <> name, 216 | "index" => 1, 217 | "index0" => 0, 218 | "rindex" => count, 219 | "rindex0" => count - 1, 220 | "length" => count, 221 | "first" => true, 222 | "last" => count == 1 223 | } 224 | end 225 | 226 | defp next_forloop( 227 | %Iterator{ 228 | forloop: %{ 229 | "name" => name, 230 | "index" => index, 231 | "index0" => index0, 232 | "rindex" => rindex, 233 | "rindex0" => rindex0, 234 | "length" => length 235 | } 236 | }, 237 | _count 238 | ) do 239 | %{ 240 | "name" => name, 241 | "index" => index + 1, 242 | "index0" => index0 + 1, 243 | "rindex" => rindex - 1, 244 | "rindex0" => rindex0 - 1, 245 | "length" => length, 246 | "first" => false, 247 | "last" => rindex0 == 1 248 | } 249 | end 250 | end 251 | 252 | defmodule Liquid.Break do 253 | alias Liquid.Tag, as: Tag 254 | alias Liquid.Context, as: Context 255 | alias Liquid.Template, as: Template 256 | 257 | def parse(%Tag{} = tag, %Template{} = template), do: {tag, template} 258 | 259 | def render(output, %Tag{}, %Context{} = context) do 260 | {output, %{context | break: true}} 261 | end 262 | end 263 | 264 | defmodule Liquid.Continue do 265 | alias Liquid.Tag, as: Tag 266 | alias Liquid.Context, as: Context 267 | 268 | def parse(%Tag{} = tag, template), do: {tag, template} 269 | 270 | def render(output, %Tag{}, %Context{} = context) do 271 | {output, %{context | continue: true}} 272 | end 273 | end 274 | 275 | defmodule Liquid.IfChanged do 276 | alias Liquid.{Template, Block} 277 | 278 | def parse(%Block{} = block, %Template{} = t), do: {block, t} 279 | 280 | def render(output, %Block{nodelist: nodelist}, context) do 281 | case context.registers["changed"] do 282 | {l, r} when l != r -> Liquid.Render.render(output, nodelist, context) 283 | _ -> {output, context} 284 | end 285 | end 286 | end 287 | -------------------------------------------------------------------------------- /lib/liquid/tags/if_else.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.ElseIf do 2 | def parse(%Liquid.Tag{} = tag, %Liquid.Template{} = t), do: {tag, t} 3 | def render(_, _, _, _), do: raise("should never get here") 4 | end 5 | 6 | defmodule Liquid.Else do 7 | def parse(%Liquid.Tag{} = tag, %Liquid.Template{} = t), do: {tag, t} 8 | def render(_, _, _, _), do: raise("should never get here") 9 | end 10 | 11 | defmodule Liquid.IfElse do 12 | alias Liquid.Condition 13 | alias Liquid.Render 14 | alias Liquid.Block 15 | alias Liquid.Tag 16 | alias Liquid.Template 17 | 18 | def syntax, 19 | do: ~r/(#{Liquid.quoted_fragment()})\s*([=!<>a-z_]+)?\s*(#{Liquid.quoted_fragment()})?/ 20 | 21 | def expressions_and_operators do 22 | ~r/(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{ 23 | Liquid.quoted_fragment() 24 | }|\S+)\s*)+)/ 25 | end 26 | 27 | def parse(%Block{} = block, %Template{} = t) do 28 | block = parse_conditions(block) 29 | 30 | case Block.split(block, [:else, :elsif]) do 31 | {true_block, [%Tag{name: :elsif, markup: markup} | elsif_block]} -> 32 | {elseif, t} = 33 | parse( 34 | %Block{ 35 | name: :if, 36 | markup: markup, 37 | nodelist: elsif_block, 38 | blank: Blank.blank?(elsif_block) 39 | }, 40 | t 41 | ) 42 | 43 | {%{block | nodelist: true_block, elselist: [elseif], blank: Blank.blank?(true_block)}, t} 44 | 45 | {true_block, [%Tag{name: :else} | false_block]} -> 46 | blank? = Blank.blank?(true_block) && Blank.blank?(false_block) 47 | {%{block | nodelist: true_block, elselist: false_block, blank: blank?}, t} 48 | 49 | {_, []} -> 50 | {%{block | blank: Blank.blank?(block.nodelist)}, t} 51 | end 52 | end 53 | 54 | def render(output, %Tag{}, context) do 55 | {output, context} 56 | end 57 | 58 | def render(output, %Block{blank: true} = block, context) do 59 | {_, context} = render(output, %{block | blank: false}, context) 60 | {output, context} 61 | end 62 | 63 | def render( 64 | output, 65 | %Block{condition: condition, nodelist: nodelist, elselist: elselist, blank: false}, 66 | context 67 | ) do 68 | condition = Condition.evaluate(condition, context) 69 | conditionlist = if condition, do: nodelist, else: elselist 70 | Render.render(output, conditionlist, context) 71 | end 72 | 73 | defp split_conditions(expressions) do 74 | expressions 75 | |> List.flatten() 76 | |> Enum.map(&String.trim/1) 77 | |> Enum.map(fn x -> 78 | case syntax() |> Regex.scan(x) do 79 | [[_, left, operator, right]] -> {left, operator, right} 80 | [[_, x]] -> x 81 | _ -> raise Liquid.SyntaxError, message: "Check the parenthesis" 82 | end 83 | end) 84 | end 85 | 86 | defp parse_conditions(%Block{markup: markup} = block) do 87 | expressions = Regex.scan(expressions_and_operators(), markup) 88 | expressions = expressions |> split_conditions |> Enum.reverse() 89 | condition = Condition.create(expressions) 90 | # Check condition syntax 91 | Condition.evaluate(condition) 92 | %{block | condition: condition} 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/liquid/tags/increment.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Increment do 2 | alias Liquid.Tag 3 | alias Liquid.Template 4 | alias Liquid.Context 5 | alias Liquid.Variable 6 | 7 | def parse(%Tag{} = tag, %Template{} = template) do 8 | {tag, template} 9 | end 10 | 11 | def render(output, %Tag{markup: markup}, %Context{} = context) do 12 | variable = Variable.create(markup) 13 | {rendered, context} = Variable.lookup(variable, context) 14 | value = rendered || 0 15 | result_assign = context.assigns |> Map.put(markup, value + 1) 16 | context = %{context | assigns: result_assign} 17 | {[value] ++ output, context} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/liquid/tags/raw.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Raw do 2 | alias Liquid.Template 3 | alias Liquid.Render 4 | alias Liquid.Block 5 | 6 | def full_token_possibly_invalid, 7 | do: ~r/\A(.*)#{Liquid.tag_start()}\s*(\w+)\s*(.*)?#{Liquid.tag_end()}\z/m 8 | 9 | def parse(%Block{name: name} = block, [h | t], accum, %Template{} = template) do 10 | if Regex.match?(Liquid.Raw.full_token_possibly_invalid(), h) do 11 | block_delimiter = "end" <> to_string(name) 12 | 13 | regex_result = 14 | Regex.scan(Liquid.Raw.full_token_possibly_invalid(), h, capture: :all_but_first) 15 | 16 | [extra_data, endblock | _] = regex_result |> List.flatten() 17 | 18 | if block_delimiter == endblock do 19 | extra_accum = accum ++ [extra_data] 20 | block = %{block | strict: false, nodelist: extra_accum |> Enum.filter(&(&1 != ""))} 21 | {block, t, template} 22 | else 23 | if length(t) > 0 do 24 | parse(block, t, accum ++ [h], template) 25 | else 26 | raise "No matching end for block {% #{to_string(name)} %}" 27 | end 28 | end 29 | else 30 | parse(block, t, accum ++ [h], template) 31 | end 32 | end 33 | 34 | def parse(%Block{} = block, %Template{} = t) do 35 | {block, t} 36 | end 37 | 38 | def render(output, %Block{} = block, context) do 39 | Render.render(output, block.nodelist, context) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/liquid/tags/table_row.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.TableRow do 2 | @moduledoc """ 3 | `tablerow` tag iterates over an array or collection splitting it up to a table with pre-set columns number 4 | 5 | Several useful variables are available to you within the loop. 6 | """ 7 | alias Liquid.Render 8 | alias Liquid.Block 9 | alias Liquid.Variable 10 | alias Liquid.Context 11 | alias Liquid.Expression 12 | alias Liquid.RangeLookup 13 | 14 | defmodule Iterator do 15 | defstruct name: nil, 16 | collection: nil, 17 | item: nil, 18 | cols: nil, 19 | limit: nil, 20 | offset: nil, 21 | forloop: %{} 22 | end 23 | 24 | def syntax, do: ~r/(\w+)\s+in\s+(#{Liquid.quoted_fragment()}+)/ 25 | 26 | @doc """ 27 | Parses and organises markup to set up iterator 28 | """ 29 | @spec parse(Liquid.Block, Liquid.Template) :: {Liquid.Block, Liquid.Template} 30 | def parse(%Block{nodelist: nodelist} = block, %Liquid.Template{} = t) do 31 | block = %{block | iterator: parse_iterator(block)} 32 | 33 | case Block.split(block) do 34 | {true_block, [_, false_block]} -> 35 | is_blank = Blank.blank?([true_block | false_block]) 36 | {%{block | nodelist: true_block, elselist: false_block, blank: is_blank}, t} 37 | 38 | {_, []} -> 39 | is_blank = Blank.blank?(nodelist) 40 | {%{block | blank: is_blank}, t} 41 | end 42 | end 43 | 44 | defp parse_iterator(%Block{markup: markup}) do 45 | [[_, item | [orig_collection]]] = Regex.scan(syntax(), markup) 46 | collection = Expression.parse(orig_collection) 47 | attributes = Liquid.tag_attributes() |> Regex.scan(markup) 48 | limit = attributes |> parse_attribute("limit") |> Variable.create() 49 | offset = attributes |> parse_attribute("offset", "0") |> Variable.create() 50 | 51 | cols = attributes |> parse_attribute("cols", "0") |> Variable.create() 52 | 53 | %Iterator{ 54 | name: orig_collection, 55 | item: item, 56 | collection: collection, 57 | limit: limit, 58 | offset: offset, 59 | cols: cols 60 | } 61 | end 62 | 63 | defp parse_attribute(attributes, name, default \\ "nil") do 64 | attributes 65 | |> Enum.reduce(default, fn x, ret -> 66 | case x do 67 | [_, ^name, attribute] when is_binary(attribute) -> attribute 68 | _ -> ret 69 | end 70 | end) 71 | end 72 | 73 | @doc """ 74 | Iterates through pre-set data and appends it to rendered output list 75 | Adds the HTML table rows and cols depending on the initial `cols` parameter 76 | """ 77 | @spec render(list(), %Block{}, %Context{}) :: {list(), %Context{}} 78 | def render(output, %Block{iterator: it} = block, %Context{} = context) do 79 | {list, context} = parse_collection(it.collection, context) 80 | list = if is_binary(list) and list != "", do: [list], else: list 81 | 82 | if is_list(list) do 83 | {limit, context} = lookup_limit(it, context) 84 | {offset, context} = lookup_offset(it, context) 85 | {new_output, context} = each([], [make_ref(), limit, offset], list, block, context) 86 | {["\n" | [new_output | ["\n"]]] ++ output, context} 87 | else 88 | if list == "" do 89 | {["\n", "\n"] ++ output, context} 90 | else 91 | Render.render(output, block.elselist, context) 92 | end 93 | end 94 | end 95 | 96 | defp parse_collection(list, context) when is_list(list), do: {list, context} 97 | 98 | defp parse_collection(%Variable{} = variable, context), do: Variable.lookup(variable, context) 99 | 100 | defp parse_collection(%RangeLookup{} = range, context), 101 | do: {RangeLookup.parse(range, context), context} 102 | 103 | defp each(output, _, [], %Block{} = block, %Context{} = context), 104 | do: {output, remember_limit(block, context)} 105 | 106 | defp each( 107 | output, 108 | [prev, limit, offset], 109 | [h | t] = list, 110 | %Block{iterator: it} = block, 111 | %Context{assigns: assigns} = context 112 | ) do 113 | forloop = next_forloop(it, list, offset, limit) 114 | block = %{block | iterator: %{it | forloop: forloop}} 115 | 116 | assigns = 117 | assigns 118 | |> Map.put("tablerowloop", forloop) 119 | |> Map.put(it.item, h) 120 | |> Map.put("changed", {prev, h}) 121 | 122 | {output_addition, block_context} = render_content(block, context, assigns, limit, offset) 123 | 124 | output = output_addition ++ output 125 | t = if block_context.break == true, do: [], else: t 126 | each(output, [h, limit, offset], t, block, %{context | assigns: block_context.assigns}) 127 | end 128 | 129 | defp render_content(%Block{iterator: it} = block, context, assigns, limit, offset) do 130 | case {should_render?(limit, offset, it.forloop["index"]), block.blank} do 131 | {true, true} -> 132 | {_, new_context} = Render.render([], block.nodelist, %{context | assigns: assigns}) 133 | {[], new_context} 134 | 135 | {true, _} -> 136 | {rendered, new_context} = Render.render([], block.nodelist, %{context | assigns: assigns}) 137 | {rendered |> add_rows_data(it.forloop), new_context} 138 | 139 | _ -> 140 | {[], context} 141 | end 142 | end 143 | 144 | defp add_rows_data(output, forloop) do 145 | output = [""] ++ output ++ [""] 146 | 147 | if forloop["col_last"] and not forloop["last"], 148 | do: ["\n"] ++ output, 149 | else: output 150 | end 151 | 152 | defp remember_limit(%Block{iterator: it}, context) do 153 | {rendered, context} = lookup_limit(it, context) 154 | limit = rendered || 0 155 | remembered = context.offsets[it.name] || 0 156 | %{context | offsets: context.offsets |> Map.put(it.name, remembered + limit)} 157 | end 158 | 159 | defp should_render?(_limit, offset, index) when index <= offset, do: false 160 | defp should_render?(nil, _, _), do: true 161 | defp should_render?(limit, offset, index) when index > limit + offset, do: false 162 | defp should_render?(_limit, _offset, _index), do: true 163 | 164 | defp lookup_limit(%Iterator{limit: limit}, %Context{} = context), 165 | do: Variable.lookup(limit, context) 166 | 167 | defp lookup_offset(%Iterator{offset: %Variable{name: "continue"}} = it, %Context{} = context), 168 | do: {context.offsets[it.name] || 0, context} 169 | 170 | defp lookup_offset(%Iterator{offset: offset}, %Context{} = context), 171 | do: Variable.lookup(offset, context) 172 | 173 | defp next_forloop(%Iterator{forloop: loop} = it, count, _, _) when map_size(loop) < 1 do 174 | count = count |> Enum.count() 175 | 176 | %{ 177 | "name" => it.item <> "-" <> it.name, 178 | "index" => 1, 179 | "index0" => 0, 180 | "col" => 1, 181 | "col0" => 1, 182 | "row" => 1, 183 | "rindex" => count, 184 | "rindex0" => count - 1, 185 | "length" => count, 186 | "first" => true, 187 | "last" => count == 1, 188 | "col_first" => true, 189 | "col_last" => count == 1 190 | } 191 | end 192 | 193 | defp next_forloop(%Iterator{forloop: loop} = it, _count, offset, limit) do 194 | {new_col, col_last, row} = get_loop_indexes(loop, it.cols.literal, offset) 195 | 196 | new_loop = %{ 197 | "name" => it.item <> "-" <> it.name, 198 | "index" => loop["index"] + 1, 199 | "index0" => loop["index0"] + 1, 200 | "col" => new_col, 201 | "col0" => loop["col0"] + 1, 202 | "row" => row, 203 | "rindex" => loop["rindex"] - 1, 204 | "rindex0" => loop["rindex0"] - 1, 205 | "length" => loop["length"], 206 | "first" => false, 207 | "last" => loop["rindex0"] == 1, 208 | "col_first" => new_col == 1, 209 | "col_last" => col_last 210 | } 211 | 212 | new_loop = 213 | if not is_nil(limit) and loop["index"] + 1 == limit + offset, 214 | do: %{new_loop | "last" => true}, 215 | else: new_loop 216 | 217 | new_loop 218 | end 219 | 220 | defp get_loop_indexes(%{"index" => index} = loop, cols, offset) 221 | when index > offset and cols == 0 do 222 | {loop["col"] + 1, loop["rindex0"] == 1, 1} 223 | end 224 | 225 | defp get_loop_indexes(%{"index" => index} = loop, val, offset) when index > offset do 226 | remainder = rem(loop["col"], val) 227 | 228 | {remainder + 1, loop["rindex0"] == 1 or remainder == val - 1, 229 | div(loop["index"] - offset, val) + 1} 230 | end 231 | 232 | defp get_loop_indexes(loop, _cols, _offset), do: {loop["col"], false, loop["row"]} 233 | end 234 | -------------------------------------------------------------------------------- /lib/liquid/tags/unless.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Unless do 2 | alias Liquid.IfElse 3 | alias Liquid.Block 4 | alias Liquid.Template 5 | alias Liquid.Condition 6 | alias Liquid.Tag 7 | alias Liquid.Render 8 | 9 | def parse(%Block{} = block, %Template{} = t) do 10 | IfElse.parse(block, t) 11 | end 12 | 13 | def render(output, %Tag{}, context) do 14 | {output, context} 15 | end 16 | 17 | def render( 18 | output, 19 | %Block{condition: condition, nodelist: nodelist, elselist: elselist}, 20 | context 21 | ) do 22 | condition = Condition.evaluate(condition, context) 23 | conditionlist = if condition, do: elselist, else: nodelist 24 | Render.render(output, conditionlist, context) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/liquid/template.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Template do 2 | @moduledoc """ 3 | Main Liquid module, all further render and parse processing passes through it 4 | """ 5 | 6 | defstruct root: nil, presets: %{}, blocks: [], errors: [] 7 | alias Liquid.{Template, Render, Context} 8 | 9 | @doc """ 10 | Function that renders passed template and context to string 11 | """ 12 | @file "render.ex" 13 | @spec render(Liquid.Template, map) :: String.t() 14 | def render(t, c \\ %{}) 15 | 16 | def render(%Template{} = t, %Context{} = c) do 17 | c = %{c | blocks: t.blocks} 18 | c = %{c | presets: t.presets} 19 | c = %{c | template: t} 20 | Render.render(t, c) 21 | end 22 | 23 | def render(%Template{} = t, assigns), do: render(t, assigns, []) 24 | 25 | def render(_, _) do 26 | raise Liquid.SyntaxError, message: "You can use only maps/structs to hold context data" 27 | end 28 | 29 | def render(%Template{} = t, %Context{global_filter: _global_filter} = context, options) do 30 | registers = Keyword.get(options, :registers, %{}) 31 | context = %{context | registers: registers} 32 | render(t, context) 33 | end 34 | 35 | def render(%Template{} = t, assigns, options) when is_map(assigns) do 36 | context = %Context{assigns: assigns} 37 | 38 | context = 39 | case {Map.has_key?(assigns, "global_filter"), Map.has_key?(assigns, :global_filter)} do 40 | {true, _} -> 41 | %{context | global_filter: Map.fetch!(assigns, "global_filter")} 42 | 43 | {_, true} -> 44 | %{context | global_filter: Map.fetch!(assigns, :global_filter)} 45 | 46 | _ -> 47 | %{ 48 | context 49 | | global_filter: Application.get_env(:liquid, :global_filter), 50 | extra_tags: Application.get_env(:liquid, :extra_tags, %{}) 51 | } 52 | end 53 | 54 | render(t, context, options) 55 | end 56 | 57 | @doc """ 58 | Function to parse markup with given presets (if any) 59 | """ 60 | @spec parse(String.t(), map) :: Liquid.Template 61 | def parse(value, presets \\ %{}) 62 | 63 | def parse(<>, presets) do 64 | Liquid.Parse.parse(markup, %Template{presets: presets}) 65 | end 66 | 67 | @spec parse(nil, map) :: Liquid.Template 68 | def parse(nil, presets) do 69 | Liquid.Parse.parse("", %Template{presets: presets}) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/liquid/utils.ex: -------------------------------------------------------------------------------- 1 | # relies to https://github.com/elixir-lang/plug/blob/master/lib/plug/html.ex 2 | defmodule Liquid.HTML do 3 | @moduledoc """ 4 | Conveniences for generating HTML. 5 | """ 6 | 7 | @doc ~S""" 8 | Escapes the given HTML. 9 | 10 | iex> Plug.HTML.html_escape("") 11 | "<foo>" 12 | 13 | iex> Plug.HTML.html_escape("quotes: \" & \'") 14 | "quotes: " & '" 15 | """ 16 | def html_escape(data) when is_binary(data) do 17 | IO.iodata_to_binary(for <>, do: escape_char(char)) 18 | end 19 | 20 | @compile {:inline, escape_char: 1} 21 | 22 | @escapes [ 23 | {?<, "<"}, 24 | {?>, ">"}, 25 | {?&, "&"}, 26 | {?", """}, 27 | {?', "'"} 28 | ] 29 | @escapes_map %{"<" => "<", ">" => ">", "&" => "&", "\"" => """, "'" => "'"} 30 | 31 | @escape_regex ~r/["><']|&(?!([a-zA-Z]+|(#\d+));)/ 32 | 33 | def html_escape_once(data) when is_binary(data) do 34 | Regex.replace(@escape_regex, data, fn v, _ -> @escapes_map[v] end) 35 | end 36 | 37 | Enum.each(@escapes, fn {match, insert} -> 38 | defp escape_char(unquote(match)), do: unquote(insert) 39 | end) 40 | 41 | defp escape_char(char), do: char 42 | end 43 | 44 | defmodule Liquid.Utils do 45 | @moduledoc """ 46 | A number of useful utils for liquid parser/filters 47 | """ 48 | 49 | @doc """ 50 | Converts various input to number for further processing 51 | """ 52 | def to_number(nil), do: 0 53 | 54 | def to_number(input) when is_number(input), do: input 55 | 56 | def to_number(input) when is_binary(input) do 57 | case Integer.parse(input) do 58 | {integer, ""} -> 59 | integer 60 | 61 | :error -> 62 | 0 63 | 64 | {integer, remainder} -> 65 | case Float.parse(input) do 66 | {_, float_remainder} when float_remainder == remainder -> 67 | integer 68 | 69 | {float, _} -> 70 | float 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/liquid/variable.ex: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Variable do 2 | @moduledoc """ 3 | Module to create and lookup for Variables 4 | 5 | """ 6 | defstruct name: nil, literal: nil, filters: [], parts: [] 7 | alias Liquid.{Appointer, Filters, Variable, Context} 8 | 9 | @doc """ 10 | resolves data from `Liquid.Variable.parse/1` and creates a variable struct 11 | """ 12 | def create(markup) when is_binary(markup) do 13 | [name | filters] = markup |> parse 14 | name = String.trim(name) 15 | variable = %Liquid.Variable{name: name, filters: filters} 16 | parsed = Liquid.Appointer.parse_name(name) 17 | 18 | if String.contains?(name, "%") do 19 | raise Liquid.SyntaxError, message: "Invalid variable name" 20 | end 21 | 22 | Map.merge(variable, parsed) 23 | end 24 | 25 | @doc """ 26 | Assigns context to variable and than applies all filters 27 | """ 28 | @spec lookup(%Variable{}, %Context{}) :: {String.t(), %Context{}} 29 | def lookup(%Variable{} = v, %Context{} = context) do 30 | {ret, filters} = Appointer.assign(v, context) 31 | 32 | result = 33 | try do 34 | {:ok, filters |> Filters.filter(ret) |> apply_global_filter(context)} 35 | rescue 36 | e in UndefinedFunctionError -> {e, e.reason} 37 | e in ArgumentError -> {e, e.message} 38 | e in ArithmeticError -> {e, "Liquid error: #{e.message}"} 39 | end 40 | 41 | case result do 42 | {:ok, text} -> {text, context} 43 | {error, message} -> process_error(context, error, message) 44 | end 45 | end 46 | 47 | defp process_error(%Context{template: template} = context, error, message) do 48 | error_mode = Application.get_env(:liquid, :error_mode, :lax) 49 | 50 | case error_mode do 51 | :lax -> 52 | {message, context} 53 | 54 | :strict -> 55 | context = %{context | template: %{template | errors: template.errors ++ [error]}} 56 | {nil, context} 57 | end 58 | end 59 | 60 | defp apply_global_filter(input, %Context{global_filter: nil}), do: input 61 | 62 | defp apply_global_filter(input, %Context{global_filter: global_filter}), 63 | do: global_filter.(input) 64 | 65 | @doc """ 66 | Parses the markup to a list of filters 67 | """ 68 | def parse(markup) when is_binary(markup) do 69 | parsed_variable = 70 | if markup != "" do 71 | Liquid.filter_parser() 72 | |> Regex.scan(markup) 73 | |> List.flatten() 74 | |> Enum.map(&String.trim/1) 75 | else 76 | [""] 77 | end 78 | 79 | if hd(parsed_variable) == "|" or hd(Enum.reverse(parsed_variable)) == "|" do 80 | raise Liquid.SyntaxError, message: "You cannot use an empty filter" 81 | end 82 | 83 | [name | filters] = Enum.filter(parsed_variable, &(&1 != "|")) 84 | 85 | filters = parse_filters(filters) 86 | [name | filters] 87 | end 88 | 89 | defp parse_filters(filters) do 90 | for markup <- filters do 91 | [_, filter] = ~r/\s*(\w+)/ |> Regex.scan(markup) |> hd() 92 | 93 | args = 94 | Liquid.filter_arguments() 95 | |> Regex.scan(markup) 96 | |> List.flatten() 97 | |> Liquid.List.even_elements() 98 | 99 | [String.to_atom(filter), args] 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/protocols/blank.ex: -------------------------------------------------------------------------------- 1 | defprotocol Blank do 2 | @doc "Returns true when blank" 3 | def blank?(data) 4 | end 5 | 6 | defimpl Blank, for: List do 7 | alias Liquid.Block 8 | alias Liquid.Tag 9 | 10 | def blank?([]), do: true 11 | 12 | def blank?(list) do 13 | list 14 | |> Enum.all?(fn 15 | x when is_binary(x) -> !!Regex.match?(~r/\A\s*\z/, x) 16 | %Block{blank: true} -> true 17 | %Tag{blank: true} -> true 18 | _ -> false 19 | end) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/protocols/matcher.ex: -------------------------------------------------------------------------------- 1 | defprotocol Liquid.Matcher do 2 | @fallback_to_any true 3 | @doc "Assigns context to values" 4 | def match(_, _) 5 | end 6 | 7 | defimpl Liquid.Matcher, for: Liquid.Context do 8 | @doc """ 9 | `Liquid.Matcher` protocol implementation for `Liquid.Context` 10 | """ 11 | 12 | def match(current, []), do: current 13 | 14 | def match(%{assigns: assigns, presets: presets}, [key | _] = parts) when is_binary(key) do 15 | current = 16 | cond do 17 | assigns |> Map.has_key?(key) -> assigns 18 | presets |> Map.has_key?(key) -> presets 19 | !is_nil(Map.get(assigns, key |> Liquid.Atomizer.to_existing_atom())) -> assigns 20 | !is_nil(Map.get(presets, key |> Liquid.Atomizer.to_existing_atom())) -> presets 21 | is_map(assigns) and Map.has_key?(assigns, :__struct__) -> assigns 22 | true -> nil 23 | end 24 | 25 | Liquid.Matcher.match(current, parts) 26 | end 27 | end 28 | 29 | defimpl Liquid.Matcher, for: Map do 30 | def match(current, []), do: current 31 | 32 | def match(current, ["size" | _]), do: current |> map_size 33 | 34 | def match(current, [<> | parts]) do 35 | index = index |> String.split("]") |> hd |> String.replace(Liquid.quote_matcher(), "") 36 | match(current, [index | parts]) 37 | end 38 | 39 | def match(current, [name | parts]) when is_binary(name) do 40 | current |> Liquid.Matcher.match(name) |> Liquid.Matcher.match(parts) 41 | end 42 | 43 | def match(current, key) when is_binary(key), do: current[key] 44 | end 45 | 46 | defimpl Liquid.Matcher, for: List do 47 | def match(current, []), do: current 48 | 49 | def match(current, ["size" | _]), do: current |> Enum.count() 50 | 51 | def match(current, [<> | parts]) do 52 | index = index |> String.split("]") |> hd |> String.to_integer() 53 | current |> Enum.fetch!(index) |> Liquid.Matcher.match(parts) 54 | end 55 | end 56 | 57 | defimpl Liquid.Matcher, for: Any do 58 | def match(nil, _), do: nil 59 | 60 | def match(current, []), do: current 61 | 62 | def match(true, _), do: nil 63 | 64 | @doc """ 65 | Match size for strings: 66 | """ 67 | def match(current, ["size" | _]) when is_binary(current), do: current |> String.length() 68 | 69 | @doc """ 70 | Match functions for structs: 71 | """ 72 | def match(current, [name | parts]) when is_map(current) and is_binary(name) do 73 | current |> Liquid.Matcher.match(name) |> Liquid.Matcher.match(parts) 74 | end 75 | 76 | def match(current, key) when is_map(current) and is_binary(key) do 77 | key = 78 | if Map.has_key?(current, :__struct__), 79 | do: key |> Liquid.Atomizer.to_existing_atom(), 80 | else: key 81 | 82 | current |> Map.get(key) 83 | end 84 | 85 | @doc """ 86 | Matches all remaining cases 87 | """ 88 | # !is_list(current) 89 | def match(_current, key) when is_binary(key), do: nil 90 | end 91 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :liquid, 7 | version: "0.9.1", 8 | elixir: "~> 1.5", 9 | deps: deps(), 10 | name: "Liquid", 11 | description: description(), 12 | package: package(), 13 | source_url: "https://github.com/bettyblocks/liquid-elixir", 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: [ 16 | coveralls: :test, 17 | "coveralls.detail": :test, 18 | "coveralls.post": :test, 19 | "coveralls.html": :test 20 | ] 21 | ] 22 | end 23 | 24 | # Configuration for the OTP application 25 | def application do 26 | [mod: {Liquid, []}] 27 | end 28 | 29 | # Returns the list of dependencies in the format: 30 | # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" } 31 | defp deps do 32 | [ 33 | {:credo, "~> 0.9.0 or ~> 1.0", only: [:dev, :test]}, 34 | {:benchee, "~> 0.11", only: :dev}, 35 | {:benchfella, "~> 0.3", only: [:dev, :test]}, 36 | {:timex, "~> 3.0"}, 37 | {:excoveralls, "~> 0.8", only: :test}, 38 | {:jason, "~> 1.1", only: [:dev, :test]}, 39 | {:ex_doc, ">= 0.0.0", only: :dev} 40 | ] 41 | end 42 | 43 | defp description do 44 | """ 45 | Liquid implementation in elixir 46 | """ 47 | end 48 | 49 | defp package do 50 | [ 51 | files: ["lib", "README*", "mix.exs"], 52 | maintainers: ["Peter Arentsen"], 53 | licenses: ["MIT"], 54 | links: %{"GitHub" => "https://github.com/nulian/liquid-elixir"} 55 | ] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 5 | "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, 7 | "credo": {:hex, :credo, "1.0.0", "aaa40fdd0543a0cf8080e8c5949d8c25f0a24e4fc8c1d83d06c388f5e5e0ea42", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm"}, 9 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, 10 | "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "excoveralls": {:hex, :excoveralls, "0.10.4", "b86230f0978bbc630c139af5066af7cd74fd16536f71bc047d1037091f9f63a9", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, 13 | "hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 16 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 18 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 19 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 21 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 22 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 23 | "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, 24 | "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 26 | } 27 | -------------------------------------------------------------------------------- /test/integration/cases_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Liquid.Test.Integration.CasesTest do 2 | use ExUnit.Case, async: true 3 | import Liquid.Helpers 4 | 5 | @cases_dir "test/templates" 6 | @levels ["simple", "medium", "complex"] 7 | @data "#{@cases_dir}/db.json" 8 | |> File.read!() 9 | |> Jason.decode!() 10 | 11 | for level <- @levels, test_case <- File.ls!("#{@cases_dir}/#{level}") do 12 | test "case #{level} - #{test_case}" do 13 | input_liquid = File.read!("#{@cases_dir}/#{unquote(level)}/#{unquote(test_case)}/input.liquid") 14 | expected_output = File.read!("#{@cases_dir}/#{unquote(level)}/#{unquote(test_case)}/output.html") 15 | liquid_output = render(input_liquid, @data) 16 | assert liquid_output == expected_output 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/liquid/block_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.BlockTest do 4 | use ExUnit.Case 5 | 6 | defmodule TestBlock do 7 | def parse(b, p), do: {b, p} 8 | end 9 | 10 | defmodule TestTag do 11 | def parse(b, p), do: {b, p} 12 | end 13 | 14 | setup_all do 15 | Liquid.start() 16 | :ok 17 | end 18 | 19 | test "blankspace" do 20 | template = Liquid.Template.parse(" ") 21 | assert template.root.nodelist == [" "] 22 | end 23 | 24 | test "variable beginning" do 25 | template = Liquid.Template.parse("{{funk}} ") 26 | assert 2 == Enum.count(template.root.nodelist) 27 | assert [%Liquid.Variable{name: name}, <>] = template.root.nodelist 28 | assert name == "funk" 29 | assert string == " " 30 | end 31 | 32 | test "variable end" do 33 | template = Liquid.Template.parse(" {{funk}}") 34 | assert 2 == Enum.count(template.root.nodelist) 35 | assert [<<_::binary>>, %Liquid.Variable{name: name}] = template.root.nodelist 36 | assert name == "funk" 37 | end 38 | 39 | test "variable middle" do 40 | template = Liquid.Template.parse(" {{funk}} ") 41 | assert 3 == Enum.count(template.root.nodelist) 42 | assert [<<_::binary>>, %Liquid.Variable{name: name}, <<_::binary>>] = template.root.nodelist 43 | assert name == "funk" 44 | end 45 | 46 | test "variable many embedded fragments" do 47 | template = Liquid.Template.parse(" {{funk}} {{so}} {{brother}} ") 48 | assert 7 == Enum.count(template.root.nodelist) 49 | 50 | assert [ 51 | <<_::binary>>, 52 | %Liquid.Variable{}, 53 | <<_::binary>>, 54 | %Liquid.Variable{}, 55 | <<_::binary>>, 56 | %Liquid.Variable{}, 57 | <<_::binary>> 58 | ] = template.root.nodelist 59 | end 60 | 61 | test "with block" do 62 | template = Liquid.Template.parse(" {% comment %} {% endcomment %} ") 63 | assert 3 == Enum.count(template.root.nodelist) 64 | assert [<<_::binary>>, %Liquid.Block{}, <<_::binary>>] = template.root.nodelist 65 | end 66 | 67 | test "registering custom tags/blocks" do 68 | Liquid.Registers.register("test", TestTag, Liquid.Tag) 69 | assert {TestTag, Liquid.Tag} = Liquid.Registers.lookup("test") 70 | end 71 | 72 | test "with custom block" do 73 | Liquid.Registers.register("testblock", TestBlock, Liquid.Block) 74 | template = Liquid.Template.parse("{% testblock %}{% endtestblock %}") 75 | assert [%Liquid.Block{name: :testblock}] = template.root.nodelist 76 | end 77 | 78 | test "with custom tag" do 79 | Liquid.Registers.register("testtag", TestTag, Liquid.Tag) 80 | template = Liquid.Template.parse("{% testtag %}") 81 | assert [%Liquid.Tag{name: :testtag}] = template.root.nodelist 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/liquid/capture_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.CaptureTest do 4 | use ExUnit.Case 5 | alias Liquid.Template 6 | 7 | setup_all do 8 | Liquid.start() 9 | on_exit(fn -> Liquid.stop() end) 10 | :ok 11 | end 12 | 13 | test :test_captures_block_content_in_variable do 14 | assert_template_result( 15 | "test string", 16 | "{% capture 'var' %}test string{% endcapture %}{{var}}", 17 | %{} 18 | ) 19 | end 20 | 21 | test :test_capture_with_hyphen_in_variable_name do 22 | template_source = """ 23 | {% capture this-thing %}Print this-thing{% endcapture %} 24 | {{ this-thing }} 25 | """ 26 | 27 | template = Template.parse(template_source) 28 | {:ok, result, _} = Template.render(template) 29 | assert "Print this-thing" == result |> String.trim() 30 | end 31 | 32 | test :test_capture_to_variable_from_outer_scope_if_existing do 33 | template_source = """ 34 | {% assign var = '' %} 35 | {% if true %} 36 | {% capture var %}first-block-string{% endcapture %} 37 | {% endif %} 38 | {% if true %} 39 | {% capture var %}test-string{% endcapture %} 40 | {% endif %} 41 | {{var}} 42 | """ 43 | 44 | template = Template.parse(template_source) 45 | {:ok, result, _} = Template.render(template) 46 | assert "test-string" == Regex.replace(~r/\s/, result, "") 47 | end 48 | 49 | test :test_assigning_from_capture do 50 | template_source = """ 51 | {% assign first = '' %} 52 | {% assign second = '' %} 53 | {% for number in (1..3) %} 54 | {% capture first %}{{number}}{% endcapture %} 55 | {% assign second = first %} 56 | {% endfor %} 57 | {{ first }}-{{ second }} 58 | """ 59 | 60 | template = Template.parse(template_source) 61 | {:ok, result, _} = Template.render(template) 62 | assert "3-3" == Regex.replace(~r/\n/, result, "") 63 | end 64 | 65 | defp assert_template_result(expected, markup, assigns) do 66 | assert_result(expected, markup, assigns) 67 | end 68 | 69 | defp assert_result(expected, markup, assigns) do 70 | template = Liquid.Template.parse(markup) 71 | {:ok, result, _} = Liquid.Template.render(template, assigns) 72 | assert result == expected 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/liquid/condition_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule ConditionTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Condition, as: Condition 7 | 8 | test :basic_condition do 9 | assert_evaluates_false("1", "==", "2") 10 | assert_evaluates_true("1", "==", "1") 11 | end 12 | 13 | test :default_operators_evaluate_true do 14 | assert_evaluates_true("1", "==", "1") 15 | assert_evaluates_true("1", "!=", "2") 16 | assert_evaluates_true("1", "<>", "2") 17 | assert_evaluates_true("1", "<", "2") 18 | assert_evaluates_true("2", ">", "1") 19 | assert_evaluates_true("1", ">=", "1") 20 | assert_evaluates_true("2", ">=", "1") 21 | assert_evaluates_true("1", "<=", "2") 22 | assert_evaluates_true("1", "<=", "1") 23 | # negative numbers 24 | assert_evaluates_true("1", ">", "-1") 25 | assert_evaluates_true("-1", "<", "1") 26 | assert_evaluates_true("1.0", ">", "-1.0") 27 | assert_evaluates_true("-1.0", "<", "1.0") 28 | end 29 | 30 | test :default_operators_evalute_false do 31 | assert_evaluates_false("1", "==", "2") 32 | assert_evaluates_false("1", "!=", "1") 33 | assert_evaluates_false("1", "<>", "1") 34 | assert_evaluates_false("1", "<", "0") 35 | assert_evaluates_false("2", ">", "4") 36 | assert_evaluates_false("1", ">=", "3") 37 | assert_evaluates_false("2", ">=", "4") 38 | assert_evaluates_false("1", "<=", "0") 39 | assert_evaluates_false("1", "<=", "0") 40 | end 41 | 42 | test :contains_works_on_strings do 43 | assert_evaluates_true("'bob'", "contains", "'o'") 44 | assert_evaluates_true("'bob'", "contains", "'b'") 45 | assert_evaluates_true("'bob'", "contains", "'bo'") 46 | assert_evaluates_true("'bob'", "contains", "'ob'") 47 | assert_evaluates_true("'bob'", "contains", "'bob'") 48 | 49 | assert_evaluates_false("'bob'", "contains", "'bob2'") 50 | assert_evaluates_false("'bob'", "contains", "'a'") 51 | assert_evaluates_false("'bob'", "contains", "'---'") 52 | end 53 | 54 | test :contains_works_on_arrays do 55 | assigns = %{"array" => [1, 2, 3, 4, 5]} 56 | 57 | assert_evaluates_false("array", "contains", "0", assigns) 58 | assert_evaluates_true("array", "contains", "1", assigns) 59 | assert_evaluates_true("array", "contains", "2", assigns) 60 | assert_evaluates_true("array", "contains", "3", assigns) 61 | assert_evaluates_true("array", "contains", "4", assigns) 62 | assert_evaluates_true("array", "contains", "5", assigns) 63 | assert_evaluates_false("array", "contains", "6", assigns) 64 | assert_evaluates_false("array", "contains", "\"1\"", assigns) 65 | end 66 | 67 | test :contains_returns_false_for_nil_operands do 68 | assert_evaluates_false("not_assigned", "contains", "0") 69 | assert_evaluates_false("0", "contains", "not_assigned") 70 | end 71 | 72 | test :or_condition do 73 | condition = Condition.create({"1", "==", "2"}) 74 | assert false == Condition.evaluate(condition) 75 | condition = Condition.join(:or, condition, {"2", "==", "2"}) 76 | assert true == Condition.evaluate(condition) 77 | condition = Condition.join(:or, condition, {"2", "==", "1"}) 78 | assert true == Condition.evaluate(condition) 79 | end 80 | 81 | test :and_condition do 82 | condition = Condition.create({"2", "==", "1"}) 83 | assert false == Condition.evaluate(condition) 84 | condition = Condition.join(:and, condition, {"2", "==", "2"}) 85 | assert false == Condition.evaluate(condition) 86 | 87 | condition = Condition.create({"2", "==", "2"}) 88 | assert true == Condition.evaluate(condition) 89 | condition = Condition.join(:and, condition, {"2", "==", "1"}) 90 | assert false == Condition.evaluate(condition) 91 | end 92 | 93 | # # test :should_allow_custom_proc_operator do 94 | # # Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ ~r{^#{right}} } 95 | 96 | # # assert_evaluates_true "'bob'", 'starts_with', "'b'" 97 | # # assert_evaluates_false "'bob'", 'starts_with', "'o'" 98 | 99 | # # ensure 100 | # # Condition.operators.delete 'starts_with' 101 | # # end 102 | 103 | test :left_or_right_may_contain_operators do 104 | assign = "gnomeslab-and-or-liquid" 105 | assigns = %{"one" => assign, "another" => assign} 106 | assert_evaluates_true("one", "==", "another", assigns) 107 | end 108 | 109 | defp assert_evaluates_true(left, op, right, assigns \\ %{}) do 110 | condition = Condition.create({left, op, right}) 111 | context = %Liquid.Context{assigns: assigns, presets: %{}} 112 | evaled = Condition.evaluate(condition, context) 113 | unless evaled, do: IO.puts("Evaluated false: #{left} #{op} #{right}") 114 | assert evaled 115 | end 116 | 117 | defp assert_evaluates_false(left, op, right, assigns \\ %{}) do 118 | condition = Condition.create({left, op, right}) 119 | context = %Liquid.Context{assigns: assigns, presets: %{}} 120 | evaled = Condition.evaluate(condition, context) 121 | unless !evaled, do: IO.puts("Evaluated true: #{left} #{op} #{right}") 122 | assert !evaled 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/liquid/custom_filter_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.CustomFilterTest do 4 | use ExUnit.Case 5 | alias Liquid.Template 6 | 7 | defmodule MyFilter do 8 | def meaning_of_life(_), do: 42 9 | end 10 | 11 | defmodule MyFilterTwo do 12 | def meaning_of_life(_), do: 40 13 | def not_meaning_of_life(_), do: 2 14 | end 15 | 16 | setup_all do 17 | Application.put_env(:liquid, :extra_filter_modules, [MyFilter, MyFilterTwo]) 18 | Liquid.start() 19 | on_exit(fn -> Liquid.stop() end) 20 | :ok 21 | end 22 | 23 | test "custom filter uses the first passed filter" do 24 | assert_template_result("42", "{{ 'whatever' | meaning_of_life }}") 25 | end 26 | 27 | test :nonexistent_in_custom_chain do 28 | assert_template_result( 29 | "2", 30 | "{{ 'text' | capitalize | not_meaning_of_life | minus_nonexistent: 1 }}" 31 | ) 32 | end 33 | 34 | test :custom_filter_in_chain do 35 | assert_template_result( 36 | "41", 37 | "{{ 'text' | upcase | nonexistent | meaning_of_life | minus: 1 }}" 38 | ) 39 | end 40 | 41 | defp assert_template_result(expected, markup, assigns \\ %{}) do 42 | assert_result(expected, markup, assigns) 43 | end 44 | 45 | defp assert_result(expected, markup, assigns) do 46 | template = Template.parse(markup) 47 | 48 | with {:ok, result, _} <- Template.render(template, assigns) do 49 | assert result == expected 50 | else 51 | {:error, message, _} -> 52 | assert message == expected 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/liquid/custom_tag_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.CustomTagTest do 4 | use ExUnit.Case 5 | alias Liquid.{Template, Tag} 6 | 7 | defmodule MinusOneTag do 8 | def parse(%Tag{} = tag, %Template{} = context) do 9 | {tag, context} 10 | end 11 | 12 | def render(output, tag, context) do 13 | number = tag.markup |> Integer.parse() |> elem(0) 14 | {["#{number - 1}"] ++ output, context} 15 | end 16 | end 17 | 18 | setup_all do 19 | Liquid.Registers.register("minus_one", MinusOneTag, Tag) 20 | Liquid.start() 21 | on_exit(fn -> Liquid.stop() end) 22 | :ok 23 | end 24 | 25 | test "custom tag from example(almost random now :)" do 26 | assert_template_result("123", "123{% assign qwe = 5 %}") 27 | assert_template_result("4", "{% minus_one 5 %}") 28 | assert_template_result("a1b", "a{% minus_one 2 %}b") 29 | end 30 | 31 | defp assert_template_result(expected, markup, assigns \\ %{}) do 32 | assert_result(expected, markup, assigns) 33 | end 34 | 35 | defp assert_result(expected, markup, assigns) do 36 | template = Template.parse(markup) 37 | 38 | with {:ok, result, _} <- Template.render(template, assigns) do 39 | assert result == expected 40 | else 41 | {:error, message, _} -> 42 | assert message == expected 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/liquid/fetch_attribute_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule FetchAttributeTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Template 7 | 8 | defmodule User do 9 | defstruct name: "John", age: 27, about: [], data: %{} 10 | end 11 | 12 | defmodule Site do 13 | defstruct site: %{} 14 | end 15 | 16 | defmodule Values do 17 | defstruct input: 0, operand: 0 18 | end 19 | 20 | setup_all do 21 | Liquid.start() 22 | :ok 23 | end 24 | 25 | test 'empty test' do 26 | assert_template_result("", "{{}}") 27 | end 28 | 29 | test 'map fetch attribute' do 30 | assert_template_result("Tester", "{{user.name}}", %{"user" => %{"name" => "Tester"}}) 31 | end 32 | 33 | test 'map fetch attribute array' do 34 | assert_template_result("first", "{{ site.users[0] }}", %{ 35 | "site" => %{"users" => ["first", "second"]} 36 | }) 37 | end 38 | 39 | test 'struct fetch attribute' do 40 | assert_template_result("Tester", "{{ data.name }}", %User{:data => %{"name" => "Tester"}}) 41 | assert_template_result("John", "{{ name }}", %User{:data => %{"name" => "Tester"}}) 42 | end 43 | 44 | test 'struct fetch attribute array' do 45 | assert_template_result("first", "{{ site.users[0] }}", %Site{ 46 | site: %{"users" => ["first", "second"]} 47 | }) 48 | end 49 | 50 | test 'struct fetch attribute filter' do 51 | assert_template_result("4", "{{ input | minus:operand }}", %Values{input: 5, operand: 1}) 52 | end 53 | 54 | test 'assign map inside' do 55 | assigns = %{"arg" => %{"value" => 1}, "map" => %{"user" => %{"name" => "Tester"}}} 56 | assert_template_result("Tester1", "{{ map.user.name | append: arg.value }}", assigns) 57 | end 58 | 59 | test 'assign struct inside' do 60 | assigns = %{"arg" => %{"value" => 1}, "map" => %User{:data => %{"name" => "Tester"}}} 61 | assert_template_result("Tester1", "{{ map.data.name | append: arg.value }}", assigns) 62 | end 63 | 64 | defp assert_template_result(expected, markup, assigns \\ %{}) do 65 | assert_result(expected, markup, assigns) 66 | end 67 | 68 | defp assert_result(expected, markup, assigns) do 69 | t = Template.parse(markup) 70 | {:ok, rendered, _} = Template.render(t, assigns) 71 | assert rendered == expected 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/liquid/file_system_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule FileSystemTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.FileSystem, as: FileSystem 7 | 8 | setup_all do 9 | Liquid.start() 10 | on_exit(fn -> Liquid.stop() end) 11 | :ok 12 | end 13 | 14 | test :default do 15 | FileSystem.register(Liquid.BlankFileSystem, "/") 16 | {:error, _reason} = FileSystem.read_template_file("dummy", dummy: "smarty") 17 | end 18 | 19 | test :local do 20 | FileSystem.register(Liquid.LocalFileSystem, "/some/path") 21 | 22 | {:ok, path} = FileSystem.full_path("mypartial") 23 | assert "/some/path/_mypartial.liquid" == path 24 | 25 | {:ok, path} = FileSystem.full_path("dir/mypartial") 26 | assert "/some/path/dir/_mypartial.liquid" == path 27 | 28 | {:error, _reason} = FileSystem.full_path("../dir/mypartial") 29 | 30 | {:error, _reason} = FileSystem.full_path("/dir/../../dir/mypartial") 31 | 32 | {:error, _reason} = FileSystem.full_path("/etc/passwd") 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/liquid/global_filter_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.GlobalFilterTest do 4 | use ExUnit.Case, async: false 5 | alias Liquid.Template 6 | 7 | defmodule MyFilter do 8 | def counting_sheeps(input) when is_binary(input), do: input <> " One, two, thr.. z-zz.." 9 | def counting_bees(input) when is_binary(input), do: input <> " One, tw.. Ouch!" 10 | end 11 | 12 | setup_all do 13 | Application.put_env(:liquid, :global_filter, &MyFilter.counting_sheeps/1) 14 | Liquid.start() 15 | on_exit(fn -> Liquid.stop(Application.delete_env(:liquid, :global_filter)) end) 16 | :ok 17 | end 18 | 19 | test "env default filter applied" do 20 | assert_template_result("Initial One, two, thr.. z-zz..", "{{ 'initial' | capitalize }}") 21 | end 22 | 23 | test "preset filter overrides default applied" do 24 | assert_template_result("Initial One, tw.. Ouch!", "{{ 'initial' | capitalize }}", %{ 25 | global_filter: &MyFilter.counting_bees/1 26 | }) 27 | end 28 | 29 | defp assert_template_result(expected, markup, assigns \\ %{}) do 30 | assert_result(expected, markup, assigns) 31 | end 32 | 33 | defp assert_result(expected, markup, assigns) do 34 | template = Template.parse(markup) 35 | 36 | with {:ok, result, _} <- Template.render(template, assigns) do 37 | assert result == expected 38 | else 39 | {:error, message, _} -> 40 | assert message == expected 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/liquid/regex_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.RegexTest do 4 | use ExUnit.Case 5 | 6 | test :empty do 7 | assert_quoted_fragment([], "") 8 | end 9 | 10 | test :quote do 11 | assert_quoted_fragment(["\"arg 1\""], "\"arg 1\"") 12 | end 13 | 14 | test :words do 15 | assert_quoted_fragment(["arg1", "arg2"], "arg1 arg2") 16 | end 17 | 18 | test :tags do 19 | assert_quoted_fragment(["", ""], " ") 20 | assert_quoted_fragment([""], "") 21 | 22 | assert_quoted_fragment( 23 | ["", ""], 24 | "" 25 | ) 26 | end 27 | 28 | test :quoted_words do 29 | assert_quoted_fragment(["arg1", "arg2", "\"arg 3\""], "arg1 arg2 \"arg 3\"") 30 | assert_quoted_fragment(["arg1", "arg2", "'arg 3'"], "arg1 arg2 \'arg 3\'") 31 | end 32 | 33 | test :quoted_words_in_the_middle do 34 | assert_quoted_fragment(["arg1", "arg2", "\"arg 3\"", "arg4"], "arg1 arg2 \"arg 3\" arg4 ") 35 | end 36 | 37 | test :variable_parser do 38 | assert_variable(["var"], "var") 39 | assert_variable(["var", "method"], "var.method") 40 | assert_variable(["var", "[method]"], "var[method]") 41 | assert_variable(["var", "[method]", "[0]"], "var[method][0]") 42 | assert_variable(["var", "[\"method\"]", "[0]"], "var[\"method\"][0]") 43 | assert_variable(["var", "[method]", "[0]", "method"], "var[method][0].method") 44 | end 45 | 46 | def assert_quoted_fragment(expected, markup) do 47 | tokens = Regex.scan(~r/#{Liquid.quoted_fragment()}/, markup) |> List.flatten() 48 | assert expected == tokens 49 | end 50 | 51 | def assert_variable(expected, markup) do 52 | tokens = Regex.scan(Liquid.variable_parser(), markup) |> List.flatten() 53 | assert expected == tokens 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/liquid/strict_parse_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Liquid.StrictParseTest do 2 | use ExUnit.Case 3 | 4 | alias Liquid.{Template, SyntaxError} 5 | 6 | test "error on empty filter" do 7 | assert_syntax_error("{{|test}}") 8 | assert_syntax_error("{{test |a|b|}}") 9 | end 10 | 11 | test "meaningless parens error" do 12 | markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" 13 | assert_syntax_error("{% if #{markup} %} YES {% endif %}") 14 | end 15 | 16 | test "unexpected characters syntax error" do 17 | markup = "true && false" 18 | assert_syntax_error("{% if #{markup} %} YES {% endif %}") 19 | end 20 | 21 | test "incomplete close variable" do 22 | assert_syntax_error("TEST {{method}") 23 | end 24 | 25 | test "incomplete close tag" do 26 | assert_syntax_error("TEST {% tag }") 27 | end 28 | 29 | test "open tag without close" do 30 | assert_syntax_error("TEST {%") 31 | end 32 | 33 | test "open variable without close" do 34 | assert_syntax_error("TEST {{") 35 | end 36 | 37 | test "syntax error" do 38 | template = "{{ 16 | divided_by: 0 }}" 39 | 40 | assert "Liquid error: divided by 0" == 41 | template |> Template.parse() |> Template.render() |> elem(1) 42 | end 43 | 44 | test "missing endtag parse time error" do 45 | assert_raise RuntimeError, "No matching end for block {% for %}", fn -> 46 | Template.parse("{% for a in b %} ...") 47 | end 48 | end 49 | 50 | test "unrecognized operator" do 51 | assert_raise SyntaxError, "Unexpected character in '1 =! 2'", fn -> 52 | Template.parse("{% if 1 =! 2 %}ok{% endif %}") 53 | end 54 | 55 | assert_raise SyntaxError, "Invalid variable name", fn -> Template.parse("{{%%%}}") end 56 | end 57 | 58 | defp assert_syntax_error(markup) do 59 | assert_raise(SyntaxError, fn -> Template.parse(markup) end) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/liquid/template_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.TemplateTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Template, as: Template 7 | alias Liquid.Parse, as: Parse 8 | 9 | setup_all do 10 | Liquid.start() 11 | :ok 12 | end 13 | 14 | test :tokenize_strings do 15 | assert [" "] == Parse.tokenize(" ") 16 | assert ["hello world"] == Parse.tokenize("hello world") 17 | end 18 | 19 | test :tokenize_variables do 20 | assert ["{{funk}}"] == Parse.tokenize("{{funk}}") 21 | assert [" ", "{{funk}}", " "] == Parse.tokenize(" {{funk}} ") 22 | 23 | assert [" ", "{{funk}}", " ", "{{so}}", " ", "{{brother}}", " "] == 24 | Parse.tokenize(" {{funk}} {{so}} {{brother}} ") 25 | 26 | assert [" ", "{{ funk }}", " "] == Parse.tokenize(" {{ funk }} ") 27 | end 28 | 29 | test :tokenize_blocks do 30 | assert ["{%comment%}"] == Parse.tokenize("{%comment%}") 31 | assert [" ", "{%comment%}", " "] == Parse.tokenize(" {%comment%} ") 32 | 33 | assert [" ", "{%comment%}", " ", "{%endcomment%}", " "] == 34 | Parse.tokenize(" {%comment%} {%endcomment%} ") 35 | 36 | assert [" ", "{% comment %}", " ", "{% endcomment %}", " "] == 37 | Parse.tokenize(" {% comment %} {% endcomment %} ") 38 | end 39 | 40 | test :should_be_able_to_handle_nil_in_parse do 41 | t = Template.parse(nil) 42 | assert {:ok, "", _context} = Template.render(t) 43 | end 44 | 45 | test :returns_assigns_from_assign_tags do 46 | t = Template.parse("{% assign foo = 'from returned assigns' %}{{ foo }}") 47 | {:ok, rendered, context} = Template.render(t) 48 | assert "from returned assigns" == rendered 49 | t = Template.parse("{{ foo }}") 50 | {:ok, rendered, _} = Template.render(t, context) 51 | assert "from returned assigns" == rendered 52 | end 53 | 54 | test :instance_assigns_persist_on_same_template_parsing_between_renders do 55 | t = Template.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") 56 | {:ok, rendered, context} = Template.render(t) 57 | assert "foo" == rendered 58 | {:ok, rendered, _} = Template.render(t, context) 59 | assert "foofoo" == rendered 60 | end 61 | 62 | test :custom_assigns_do_not_persist_on_same_template do 63 | t = Template.parse("{{ foo }}") 64 | 65 | {:ok, rendered, _} = Template.render(t, %{"foo" => "from custom assigns"}) 66 | assert "from custom assigns" == rendered 67 | {:ok, rendered, _} = Template.render(t) 68 | assert "" == rendered 69 | end 70 | 71 | test :template_assigns_squash_assigns do 72 | t = Template.parse("{% assign foo = 'from instance assigns' %}{{ foo }}") 73 | {:ok, rendered, _} = Template.render(t) 74 | assert "from instance assigns" == rendered 75 | {:ok, rendered, _} = Template.render(t, %{"foo" => "from custom assigns"}) 76 | assert "from instance assigns" == rendered 77 | end 78 | 79 | test :template_assigns_squash_preset_assigns do 80 | t = 81 | Template.parse("{% assign foo = 'from instance assigns' %}{{ foo }}", %{ 82 | "foo" => "from preset assigns" 83 | }) 84 | 85 | {:ok, rendered, _} = Template.render(t) 86 | assert "from instance assigns" == rendered 87 | end 88 | 89 | test "check if you can assign registers" do 90 | t = Template.parse("{{ foo }}") 91 | 92 | {:ok, rendered, context} = 93 | Template.render(t, %{"foo" => "from assigns"}, registers: %{test: "hallo"}) 94 | 95 | assert "from assigns" == rendered 96 | assert %{test: "hallo"} == context.registers 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/liquid/variable_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.VariableTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Variable, as: Var 7 | alias Liquid.Template 8 | 9 | test :variable do 10 | v = Var.create("hello") 11 | assert "hello" == v.name 12 | end 13 | 14 | test :filters do 15 | v = Var.create("hello | textileze") 16 | assert "hello" == v.name 17 | assert [[:textileze, []]] == v.filters 18 | 19 | v = Var.create("hello | textileze | paragraph") 20 | assert "hello" == v.name 21 | assert [[:textileze, []], [:paragraph, []]] == v.filters 22 | 23 | v = Var.create("hello | strftime: '%Y'") 24 | assert "hello" == v.name 25 | assert [[:strftime, ["'%Y'"]]] == v.filters 26 | 27 | v = Var.create("'typo' | link_to: 'Typo', true ") 28 | assert "'typo'" == v.name 29 | assert [[:link_to, ["'Typo'", "true"]]] == v.filters 30 | 31 | v = Var.create("'typo' | link_to: 'Typo', false") 32 | assert "'typo'", v.name 33 | assert [[:link_to, ["'Typo'", "false"]]] == v.filters 34 | 35 | v = Var.create("'foo' | repeat: 3") 36 | assert "'foo'" == v.name 37 | assert [[:repeat, ["3"]]] == v.filters 38 | 39 | v = Var.create("'foo' | repeat: 3, 3") 40 | assert "'foo'" == v.name 41 | assert [[:repeat, ["3", "3"]]] == v.filters 42 | 43 | v = Var.create("'foo' | repeat: 3, 3, 3") 44 | assert "'foo'" == v.name 45 | assert [[:repeat, ["3", "3", "3"]]] == v.filters 46 | 47 | v = Var.create("hello | strftime: '%Y, okay?'") 48 | assert "hello" == v.name 49 | assert [[:strftime, ["'%Y, okay?'"]]] == v.filters 50 | 51 | v = Var.create("hello | things: \"%Y, okay?\", 'the other one'!") 52 | assert "hello" == v.name 53 | assert [[:things, ["\"%Y, okay?\"", "'the other one'"]]] == v.filters 54 | end 55 | 56 | test :filter_with_date_parameter do 57 | v = Var.create("'2006-06-06' | date: \"%m/%d/%Y\"") 58 | assert "'2006-06-06'" == v.name 59 | assert [[:date, ["\"%m/%d/%Y\""]]] == v.filters 60 | end 61 | 62 | test "render error mode strict/lax" do 63 | template = "{{ 16 | divided_by: 0 }}" 64 | result = template |> Template.parse() |> Template.render() |> elem(1) 65 | assert result == "Liquid error: divided by 0" 66 | 67 | Application.put_env(:liquid, :error_mode, :strict) 68 | {:ok, result, context} = template |> Template.parse() |> Template.render() 69 | assert Enum.count(context.template.errors) == 1 70 | assert context.template.errors == [%ArithmeticError{message: "divided by 0"}] 71 | assert result == "" 72 | Application.delete_env(:liquid, :error_mode) 73 | end 74 | 75 | test :filters_without_whitespace do 76 | v = Var.create("hello | textileze | paragraph") 77 | assert "hello" == v.name 78 | assert [[:textileze, []], [:paragraph, []]] == v.filters 79 | 80 | v = Var.create("hello|textileze|paragraph") 81 | assert "hello" == v.name 82 | assert [[:textileze, []], [:paragraph, []]] == v.filters 83 | end 84 | 85 | test :symbol do 86 | v = Var.create("http://disney.com/logo.gif | image: 'med'") 87 | assert "http://disney.com/logo.gif" == v.name 88 | assert [[:image, ["'med'"]]] == v.filters 89 | end 90 | 91 | test :string_single_quoted do 92 | v = Var.create(" \"hello\" ") 93 | assert "\"hello\"" == v.name 94 | end 95 | 96 | test :string_double_quoted do 97 | v = Var.create(" 'hello' ") 98 | assert "'hello'" == v.name 99 | end 100 | 101 | test :integer do 102 | v = Var.create(" 1000 ") 103 | assert "1000" == v.name 104 | end 105 | 106 | test :float do 107 | v = Var.create(" 1000.01 ") 108 | assert "1000.01" == v.name 109 | end 110 | 111 | test :string_with_special_chars do 112 | v = Var.create(" 'hello! $!@.;\"ddasd\" ' ") 113 | assert "'hello! $!@.;\"ddasd\" '" == v.name 114 | end 115 | 116 | test :string_dot do 117 | v = Var.create(" test.test ") 118 | assert "test.test" == v.name 119 | end 120 | end 121 | 122 | defmodule VariableResolutionTest do 123 | use ExUnit.Case 124 | 125 | alias Liquid.Template, as: Template 126 | 127 | setup_all do 128 | Liquid.start() 129 | on_exit(fn -> Liquid.stop() end) 130 | :ok 131 | end 132 | 133 | test :simple_variable do 134 | template = Template.parse("{{test}}") 135 | {:ok, rendered, _} = Template.render(template, %{"test" => "worked"}) 136 | assert "worked" == rendered 137 | {:ok, rendered, _} = Template.render(template, %{"test" => "worked wonderfully"}) 138 | assert "worked wonderfully" == rendered 139 | end 140 | 141 | test :simple_with_whitespaces do 142 | template = Template.parse(" {{ test }} ") 143 | {:ok, rendered, _} = Template.render(template, %{"test" => "worked"}) 144 | assert " worked " == rendered 145 | {:ok, rendered, _} = Template.render(template, %{"test" => "worked wonderfully"}) 146 | assert " worked wonderfully " == rendered 147 | end 148 | 149 | test :ignore_unknown do 150 | template = Template.parse("{{ test }}") 151 | {:ok, rendered, _} = Template.render(template) 152 | assert "" == rendered 153 | end 154 | 155 | test :hash_scoping do 156 | template = Template.parse("{{ test.test }}") 157 | {:ok, rendered, _} = Template.render(template, %{"test" => %{"test" => "worked"}}) 158 | assert "worked" == rendered 159 | end 160 | 161 | test :preset_assigns do 162 | template = Template.parse("{{ test }}", %{"test" => "worked"}) 163 | {:ok, rendered, _} = Template.render(template) 164 | assert "worked" == rendered 165 | end 166 | 167 | test :reuse_parsed_template do 168 | template = Template.parse("{{ greeting }} {{ name }}", %{"greeting" => "Goodbye"}) 169 | assert %{"greeting" => "Goodbye"} == template.presets 170 | {:ok, rendered, _} = Template.render(template, %{"greeting" => "Hello", "name" => "Tobi"}) 171 | assert "Hello Tobi" == rendered 172 | {:ok, rendered, _} = Template.render(template, %{"greeting" => "Hello", "unknown" => "Tobi"}) 173 | assert "Hello " == rendered 174 | {:ok, rendered, _} = Template.render(template, %{"greeting" => "Hello", "name" => "Brian"}) 175 | assert "Hello Brian" == rendered 176 | {:ok, rendered, _} = Template.render(template, %{"name" => "Brian"}) 177 | assert "Goodbye Brian" == rendered 178 | end 179 | 180 | test :assigns_not_polluted_from_template do 181 | template = Template.parse("{{ test }}{% assign test = 'bar' %}{{ test }}", %{"test" => "baz"}) 182 | {:ok, rendered, _} = Template.render(template) 183 | assert "bazbar" == rendered 184 | {:ok, rendered, _} = Template.render(template) 185 | assert "bazbar" == rendered 186 | {:ok, rendered, _} = Template.render(template, %{"test" => "foo"}) 187 | assert "foobar" == rendered 188 | {:ok, rendered, _} = Template.render(template) 189 | assert "bazbar" == rendered 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /test/liquid_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LiquidTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/tags/assign_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.AssignTest do 4 | use ExUnit.Case 5 | 6 | setup_all do 7 | Liquid.start() 8 | on_exit(fn -> Liquid.stop() end) 9 | :ok 10 | end 11 | 12 | test :assigned_variable do 13 | assert_result(".foo.", "{% assign foo = values %}.{{ foo[0] }}.", %{ 14 | "values" => ["foo", "bar", "baz"] 15 | }) 16 | 17 | assert_result(".bar.", "{% assign foo = values %}.{{ foo[1] }}.", %{ 18 | "values" => ["foo", "bar", "baz"] 19 | }) 20 | end 21 | 22 | test :assign_with_filter do 23 | assert_result(".bar.", "{% assign foo = values | split: ',' %}.{{ foo[1] }}.", %{ 24 | "values" => "foo,bar,baz" 25 | }) 26 | end 27 | 28 | test "assign string to var and then show" do 29 | assert_result("test", "{% assign foo = 'test' %}{{foo}}", %{}) 30 | end 31 | 32 | defp assert_result(expected, markup, assigns) do 33 | template = Liquid.Template.parse(markup) 34 | {:ok, result, _} = Liquid.Template.render(template, assigns) 35 | assert result == expected 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/tags/blank_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.BlankTest do 4 | use ExUnit.Case 5 | 6 | def n, do: "10" 7 | 8 | setup_all do 9 | Liquid.start() 10 | :ok 11 | end 12 | 13 | def wrap_in_for(body) do 14 | "{% for i in (1.." <> n() <> ") %}" <> body <> "{% endfor %}" 15 | end 16 | 17 | def wrap_in_if(body) do 18 | "{% if true %}" <> body <> "{% endif %}" 19 | end 20 | 21 | def wrap(body) do 22 | wrap_in_for(body) <> wrap_in_if(body) 23 | end 24 | 25 | test :test_loops_are_blank do 26 | assert_result("", wrap_in_for(" "), %{}) 27 | end 28 | 29 | test :test_if_else_are_blank do 30 | assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}") 31 | end 32 | 33 | test :test_unless_is_blank do 34 | assert_template_result("", wrap("{% unless true %} {% endunless %}")) 35 | end 36 | 37 | test :test_mark_as_blank_only_during_parsing do 38 | assert_template_result( 39 | String.duplicate(" ", String.to_integer(n()) + 1), 40 | wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}") 41 | ) 42 | end 43 | 44 | test :test_comments_are_blank do 45 | assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} ")) 46 | end 47 | 48 | test :test_captures_are_blank do 49 | assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} ")) 50 | end 51 | 52 | test :test_nested_blocks_are_blank_but_only_if_all_children_are do 53 | assert_template_result("", wrap(wrap(" "))) 54 | 55 | assert_template_result( 56 | String.duplicate("\n but this is not ", String.to_integer(n()) + 1), 57 | wrap( 58 | "{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}\n {% if true %} but this is not {% endif %}" 59 | ) 60 | ) 61 | end 62 | 63 | test :test_assigns_are_blank do 64 | assert_template_result("", wrap(" {% assign foo = \"bar\" %} ")) 65 | end 66 | 67 | test :loop_test do 68 | assert_template_result("testtesttesttesttesttesttesttesttesttesttest", wrap("test")) 69 | end 70 | 71 | defp assert_template_result(expected, markup) do 72 | assert_result(expected, markup, %{}) 73 | end 74 | 75 | defp assert_result(expected, markup, assigns) do 76 | template = Liquid.Template.parse(markup) 77 | {:ok, result, _} = Liquid.Template.render(template, assigns) 78 | assert result == expected 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/tags/case_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.CaseTest do 4 | use ExUnit.Case 5 | 6 | setup_all do 7 | Liquid.start() 8 | :ok 9 | end 10 | 11 | test "render first block with a matching {% when %} argument" do 12 | assert_result( 13 | " its 1 ", 14 | "{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", 15 | %{"condition" => 1} 16 | ) 17 | 18 | assert_result( 19 | " its 2 ", 20 | "{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", 21 | %{"condition" => 2} 22 | ) 23 | 24 | # dont render whitespace between case and first when 25 | assert_result( 26 | " its 2 ", 27 | "{% case condition %} {% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", 28 | %{"condition" => 2} 29 | ) 30 | end 31 | 32 | test "match strings correctly" do 33 | assert_result(" hit ", "{% case condition %}{% when \"string here\" %} hit {% endcase %}", %{ 34 | "condition" => "string here" 35 | }) 36 | 37 | assert_result("", "{% case condition %}{% when \"string here\" %} hit {% endcase %}", %{ 38 | "condition" => "bad string here" 39 | }) 40 | end 41 | 42 | test "wont render anything if no matches found" do 43 | assert_result( 44 | " ", 45 | " {% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %} ", 46 | %{"condition" => 3} 47 | ) 48 | end 49 | 50 | test "evaluate variables and expressions" do 51 | assert_result("", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{"a" => []}) 52 | assert_result("1", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{"a" => [1]}) 53 | 54 | assert_result("2", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{ 55 | "a" => [1, 1] 56 | }) 57 | 58 | assert_result("", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{ 59 | "a" => [1, 1, 1] 60 | }) 61 | 62 | assert_result("", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{ 63 | "a" => [1, 1, 1, 1] 64 | }) 65 | 66 | assert_result("", "{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", %{ 67 | "a" => [1, 1, 1, 1, 1] 68 | }) 69 | end 70 | 71 | test "allow assignment from within a {% when %} block" do 72 | # Example from the shopify forums 73 | template = 74 | "{% case collection.handle %}" <> 75 | "{% when 'menswear-jackets' %}" <> 76 | "{% assign ptitle = 'menswear' %}" <> 77 | "{% when 'menswear-t-shirts' %}" <> 78 | "{% assign ptitle = 'menswear' %}" <> 79 | "{% else %}" <> "{% assign ptitle = 'womenswear' %}" <> "{% endcase %}" <> "{{ ptitle }}" 80 | 81 | assert_result("menswear", template, %{"collection" => %{"handle" => "menswear-jackets"}}) 82 | assert_result("menswear", template, %{"collection" => %{"handle" => "menswear-t-shirts"}}) 83 | assert_result("womenswear", template, %{"collection" => %{"handle" => "x"}}) 84 | assert_result("womenswear", template, %{"collection" => %{"handle" => "y"}}) 85 | assert_result("womenswear", template, %{"collection" => %{"handle" => "z"}}) 86 | end 87 | 88 | test "allows use of 'or' to chain parameters with {% when %}" do 89 | template = 90 | "{% case condition %}" <> 91 | "{% when 1 or 2 or 3 %} its 1 or 2 or 3 " <> "{% when 4 %} its 4 {% endcase %}" 92 | 93 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 1}) 94 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 2}) 95 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 3}) 96 | assert_result(" its 4 ", template, %{"condition" => 4}) 97 | assert_result("", template, %{"condition" => 5}) 98 | 99 | template = 100 | "{% case condition %}" <> 101 | "{% when 1 or \"string\" or null %} its 1 or 2 or 3 " <> 102 | "{% when 4 %} its 4 {% endcase %}" 103 | 104 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 1}) 105 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => "string"}) 106 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => nil}) 107 | assert_result("", template, %{"condition" => "something else"}) 108 | end 109 | 110 | test "allows use of commas to chain parameters with {% when %} " do 111 | template = 112 | "{% case condition %}" <> 113 | "{% when 1, 2, 3 %} its 1 or 2 or 3 " <> "{% when 4 %} its 4 {% endcase %}" 114 | 115 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 1}) 116 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 2}) 117 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 3}) 118 | assert_result(" its 4 ", template, %{"condition" => 4}) 119 | assert_result("", template, %{"condition" => 5}) 120 | 121 | template = 122 | "{% case condition %}" <> 123 | "{% when 1, \"string\", null %} its 1 or 2 or 3 " <> "{% when 4 %} its 4 {% endcase %}" 124 | 125 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => 1}) 126 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => "string"}) 127 | assert_result(" its 1 or 2 or 3 ", template, %{"condition" => nil}) 128 | assert_result("", template, %{"condition" => "something else"}) 129 | end 130 | 131 | # test "error on bad syntax" do 132 | # # assert_raise Liquid.SyntaxError fn -> 133 | # {:error, _ } = "{% case false %}{% when %}true{% endcase %}" |> Template.parse 134 | # |> Template.render 135 | # # end 136 | 137 | # # expect { 138 | # {:error, _} = "{% case false %}{% huh %}true{% endcase %}" |> Template.parse 139 | # |> Template.render 140 | # # }.to raise_error(Liquid::SyntaxError) 141 | # end 142 | 143 | test "renders the {% else %} block when no matches found" do 144 | assert_result( 145 | " hit ", 146 | "{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}", 147 | %{"condition" => 5} 148 | ) 149 | 150 | assert_result( 151 | " else ", 152 | "{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}", 153 | %{"condition" => 6} 154 | ) 155 | end 156 | 157 | test "should evaluate variables and expressions" do 158 | assert_result( 159 | "else", 160 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 161 | %{"a" => []} 162 | ) 163 | 164 | assert_result( 165 | "1", 166 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 167 | %{"a" => [1]} 168 | ) 169 | 170 | assert_result( 171 | "2", 172 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 173 | %{"a" => [1, 1]} 174 | ) 175 | 176 | assert_result( 177 | "else", 178 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 179 | %{"a" => [1, 1, 1]} 180 | ) 181 | 182 | assert_result( 183 | "else", 184 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 185 | %{"a" => [1, 1, 1, 1]} 186 | ) 187 | 188 | assert_result( 189 | "else", 190 | "{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", 191 | %{"a" => [1, 1, 1, 1, 1]} 192 | ) 193 | 194 | assert_result( 195 | "else", 196 | "{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", 197 | %{} 198 | ) 199 | 200 | assert_result( 201 | "false", 202 | "{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", 203 | %{} 204 | ) 205 | 206 | assert_result( 207 | "true", 208 | "{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", 209 | %{} 210 | ) 211 | 212 | assert_result( 213 | "else", 214 | "{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}" 215 | ) 216 | end 217 | 218 | defp assert_result(expected, markup), do: assert_result(expected, markup, %Liquid.Context{}) 219 | 220 | defp assert_result(expected, markup, %Liquid.Context{} = context) do 221 | t = Liquid.Template.parse(markup) 222 | {:ok, rendered, _context} = Liquid.Template.render(t, context) 223 | assert expected == rendered 224 | end 225 | 226 | defp assert_result(expected, markup, assigns) do 227 | context = %Liquid.Context{assigns: assigns} 228 | assert_result(expected, markup, context) 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/tags/for_else_tag_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule ForElseTagTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Template, as: Template 7 | 8 | setup_all do 9 | Liquid.start() 10 | :ok 11 | end 12 | 13 | test :for_block do 14 | assert_result(" yo yo yo yo ", "{%for item in array%} yo {%endfor%}", %{ 15 | "array" => [1, 2, 3, 4] 16 | }) 17 | 18 | assert_result("yoyo", "{%for item in array%}yo{%endfor%}", %{"array" => [1, 2]}) 19 | assert_result(" yo ", "{%for item in array%} yo {%endfor%}", %{"array" => [1]}) 20 | assert_result("", "{%for item in array%}{%endfor%}", %{"array" => [1, 2]}) 21 | 22 | expected = """ 23 | 24 | yo 25 | 26 | yo 27 | 28 | yo 29 | 30 | """ 31 | 32 | template = """ 33 | {%for item in array%} 34 | yo 35 | {%endfor%} 36 | """ 37 | 38 | assert_result(expected, template, %{"array" => [1, 2, 3]}) 39 | end 40 | 41 | test :for_reversed do 42 | assigns = %{"array" => [1, 2, 3]} 43 | assert_result("321", "{%for item in array reversed %}{{item}}{%endfor%}", assigns) 44 | end 45 | 46 | test :for_with_range do 47 | assert_result(" 1 2 3 ", "{%for item in (1..3) %} {{item}} {%endfor%}", %{}) 48 | 49 | assert_raise(ArgumentError, fn -> 50 | markup = "{% for i in (a..2) %}{% endfor %}'" 51 | t = Template.parse(markup) 52 | Template.render(t, %{"a" => [1, 2]}) 53 | end) 54 | 55 | assert_template_result(" 0 1 2 3 ", "{% for item in (a..3) %} {{item}} {% endfor %}", %{ 56 | "a" => "invalid integer" 57 | }) 58 | end 59 | 60 | test :for_with_variable do 61 | assert_result(" 1 2 3 ", "{%for item in array%} {{item}} {%endfor%}", %{ 62 | "array" => [1, 2, 3] 63 | }) 64 | 65 | assert_result("123", "{%for item in array%}{{item}}{%endfor%}", %{"array" => [1, 2, 3]}) 66 | assert_result("123", "{% for item in array %}{{item}}{% endfor %}", %{"array" => [1, 2, 3]}) 67 | 68 | assert_result("abcd", "{%for item in array%}{{item}}{%endfor%}", %{ 69 | "array" => ["a", "b", "c", "d"] 70 | }) 71 | 72 | assert_result("a b c", "{%for item in array%}{{item}}{%endfor%}", %{ 73 | "array" => ["a", " ", "b", " ", "c"] 74 | }) 75 | 76 | assert_result("abc", "{%for item in array%}{{item}}{%endfor%}", %{ 77 | "array" => ["a", "", "b", "", "c"] 78 | }) 79 | end 80 | 81 | test :for_helpers do 82 | assigns = %{"array" => [1, 2, 3]} 83 | 84 | assert_result( 85 | " 1/3 2/3 3/3 ", 86 | "{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}", 87 | assigns 88 | ) 89 | 90 | assert_result(" 1 2 3 ", "{%for item in array%} {{forloop.index}} {%endfor%}", assigns) 91 | assert_result(" 0 1 2 ", "{%for item in array%} {{forloop.index0}} {%endfor%}", assigns) 92 | assert_result(" 2 1 0 ", "{%for item in array%} {{forloop.rindex0}} {%endfor%}", assigns) 93 | assert_result(" 3 2 1 ", "{%for item in array%} {{forloop.rindex}} {%endfor%}", assigns) 94 | 95 | assert_result( 96 | " true false false ", 97 | "{%for item in array%} {{forloop.first}} {%endfor%}", 98 | assigns 99 | ) 100 | 101 | assert_result( 102 | " false false true ", 103 | "{%for item in array%} {{forloop.last}} {%endfor%}", 104 | assigns 105 | ) 106 | end 107 | 108 | test :for_and_if do 109 | assigns = %{"array" => [1, 2, 3]} 110 | 111 | assert_result( 112 | "+--", 113 | "{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}", 114 | assigns 115 | ) 116 | end 117 | 118 | test :for_else do 119 | assert_result("+++", "{%for item in array%}+{%else%}-{%endfor%}", %{"array" => [1, 2, 3]}) 120 | assert_result("-", "{%for item in array%}+{%else%}-{%endfor%}", %{"array" => []}) 121 | assert_result("-", "{%for item in array%}+{%else%}-{%endfor%}", %{"array" => nil}) 122 | end 123 | 124 | test :limiting do 125 | assigns = %{"array" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]} 126 | assert_result("12", "{%for i in array limit:2 %}{{ i }}{%endfor%}", assigns) 127 | assert_result("1234", "{%for i in array limit:4 %}{{ i }}{%endfor%}", assigns) 128 | assert_result("3456", "{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}", assigns) 129 | assert_result("3456", "{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}", assigns) 130 | end 131 | 132 | test :dynamic_variable_limiting do 133 | assigns = %{"array" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], "limit" => 2, "offset" => 2} 134 | 135 | assert_result( 136 | "34", 137 | "{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}", 138 | assigns 139 | ) 140 | end 141 | 142 | test :nested_for do 143 | assigns = %{"array" => [[1, 2], [3, 4], [5, 6]]} 144 | 145 | assert_result( 146 | "123456", 147 | "{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}", 148 | assigns 149 | ) 150 | end 151 | 152 | test :offset_only do 153 | assigns = %{"array" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]} 154 | assert_result("890", "{%for i in array offset:7 %}{{ i }}{%endfor%}", assigns) 155 | end 156 | 157 | test :pause_resume do 158 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]}} 159 | 160 | markup = """ 161 | {%for i in array.items limit: 3 %}{{i}}{%endfor%} 162 | next 163 | {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} 164 | next 165 | {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} 166 | """ 167 | 168 | expected = """ 169 | 123 170 | next 171 | 456 172 | next 173 | 789 174 | """ 175 | 176 | assert_result(expected, markup, assigns) 177 | end 178 | 179 | test :pause_resume_limit do 180 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]}} 181 | 182 | markup = """ 183 | {%for i in array.items limit:3 %}{{i}}{%endfor%} 184 | next 185 | {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} 186 | next 187 | {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} 188 | """ 189 | 190 | expected = """ 191 | 123 192 | next 193 | 456 194 | next 195 | 7 196 | """ 197 | 198 | assert_result(expected, markup, assigns) 199 | end 200 | 201 | test :pause_resume_BIG_limit do 202 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]}} 203 | 204 | markup = """ 205 | {%for i in array.items limit:3 %}{{i}}{%endfor%} 206 | next 207 | {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} 208 | next 209 | {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} 210 | """ 211 | 212 | expected = """ 213 | 123 214 | next 215 | 456 216 | next 217 | 7890 218 | """ 219 | 220 | assert_result(expected, markup, assigns) 221 | end 222 | 223 | test :pause_resume_BIG_offset do 224 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]}} 225 | 226 | markup = """ 227 | {%for i in array.items limit:3 %}{{i}}{%endfor%} 228 | next 229 | {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} 230 | next{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%} 231 | """ 232 | 233 | expected = """ 234 | 123 235 | next 236 | 456 237 | next 238 | """ 239 | 240 | assert_result(expected, markup, assigns) 241 | end 242 | 243 | test :for_with_break do 244 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}} 245 | 246 | markup = "{% for i in array.items %}{% break %}{% endfor %}" 247 | expected = "" 248 | assert_result(expected, markup, assigns) 249 | 250 | markup = "{% for i in array.items %}{{ i }}{% break %}{% endfor %}" 251 | expected = "1" 252 | assert_result(expected, markup, assigns) 253 | 254 | markup = "{% for i in array.items %}{% break %}{{ i }}{% endfor %}" 255 | expected = "" 256 | assert_result(expected, markup, assigns) 257 | 258 | markup = "{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}" 259 | expected = "1234" 260 | assert_result(expected, markup, assigns) 261 | 262 | # test break does nothing when unreached 263 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5]}} 264 | 265 | markup = 266 | "{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}" 267 | 268 | expected = "12345" 269 | assert_result(expected, markup, assigns) 270 | end 271 | 272 | test :for_with_loop_inside_loop do 273 | # tests to ensure it only breaks out of the local for loop 274 | # and not all of them. 275 | assigns = %{"array" => [[1, 2], [3, 4], [5, 6]]} 276 | 277 | markup = 278 | "{% for item in array %}" <> 279 | "{% for i in item %}" <> "{{ i }}" <> "{% endfor %}" <> "{% endfor %}" 280 | 281 | expected = "123456" 282 | assert_result(expected, markup, assigns) 283 | end 284 | 285 | test :for_with_break_inside_loop do 286 | # tests to ensure it only breaks out of the local for loop 287 | # and not all of them. 288 | assigns = %{"array" => [[1, 2], [3, 4], [5, 6]]} 289 | 290 | markup = 291 | "{% for item in array %}" <> 292 | "{% for i in item %}" <> 293 | "{% if i == 1 %}" <> 294 | "{% break %}" <> "{% endif %}" <> "{{ i }}" <> "{% endfor %}" <> "{% endfor %}" 295 | 296 | expected = "3456" 297 | assert_result(expected, markup, assigns) 298 | end 299 | 300 | test :for_with_continue do 301 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5]}} 302 | 303 | markup = "{% for i in array.items %}{% continue %}{% endfor %}" 304 | expected = "" 305 | assert_result(expected, markup, assigns) 306 | 307 | markup = "{% for i in array.items %}{{ i }}{% continue %}{% endfor %}" 308 | expected = "12345" 309 | assert_result(expected, markup, assigns) 310 | 311 | markup = "{% for i in array.items %}{% continue %}{{ i }}{% endfor %}" 312 | expected = "" 313 | assert_result(expected, markup, assigns) 314 | 315 | markup = 316 | "{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}" 317 | 318 | expected = "123" 319 | assert_result(expected, markup, assigns) 320 | 321 | markup = 322 | "{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}" 323 | 324 | expected = "1245" 325 | assert_result(expected, markup, assigns) 326 | 327 | # tests to ensure it only continues the local for loop and not all of them. 328 | assigns = %{"array" => [[1, 2], [3, 4], [5, 6]]} 329 | 330 | markup = 331 | "{% for item in array %}" <> 332 | "{% for i in item %}" <> 333 | "{% if i == 1 %}" <> 334 | "{% continue %}" <> "{% endif %}" <> "{{ i }}" <> "{% endfor %}" <> "{% endfor %}" 335 | 336 | expected = "23456" 337 | assert_result(expected, markup, assigns) 338 | 339 | # test continue does nothing when unreached 340 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5]}} 341 | 342 | markup = 343 | "{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}" 344 | 345 | expected = "12345" 346 | assert_result(expected, markup, assigns) 347 | end 348 | 349 | test :for_tag_string do 350 | # test continue does nothing when unreached 351 | assigns = %{"array" => %{"items" => [1, 2, 3, 4, 5]}} 352 | 353 | markup = 354 | "{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}" 355 | 356 | expected = "12345" 357 | assert_result(expected, markup, assigns) 358 | 359 | assert_result("test string", "{%for val in string%}{{val}}{%endfor%}", %{ 360 | "string" => "test string" 361 | }) 362 | 363 | assert_result("test string", "{%for val in string limit:1%}{{val}}{%endfor%}", %{ 364 | "string" => "test string" 365 | }) 366 | 367 | assert_result( 368 | "val-string-1-1-0-1-0-true-true-test string", 369 | "{%for val in string%}" <> 370 | "{{forloop.name}}-" <> 371 | "{{forloop.index}}-" <> 372 | "{{forloop.length}}-" <> 373 | "{{forloop.index0}}-" <> 374 | "{{forloop.rindex}}-" <> 375 | "{{forloop.rindex0}}-" <> 376 | "{{forloop.first}}-" <> "{{forloop.last}}-" <> "{{val}}{%endfor%}", 377 | %{"string" => "test string"} 378 | ) 379 | end 380 | 381 | test :blank_string_not_iterable do 382 | assert_result("", "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", %{ 383 | "characters" => "" 384 | }) 385 | end 386 | 387 | defp assert_template_result(expected, markup, assigns) do 388 | assert_result(expected, markup, assigns) 389 | end 390 | 391 | defp assert_result(expected, markup, assigns) do 392 | t = Template.parse(markup) 393 | {:ok, rendered, _} = Template.render(t, assigns) 394 | assert rendered == expected 395 | end 396 | end 397 | -------------------------------------------------------------------------------- /test/tags/if_else_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.Tags.IfElseTagTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Template, as: Template 7 | 8 | setup_all do 9 | Liquid.start() 10 | on_exit(fn -> Liquid.stop() end) 11 | :ok 12 | end 13 | 14 | test :if_block do 15 | assert_result(" ", " {% if false %} this text should not go into the output {% endif %} ") 16 | 17 | assert_result( 18 | " this text should go into the output ", 19 | " {% if true %} this text should go into the output {% endif %} " 20 | ) 21 | 22 | assert_result( 23 | " you rock ?", 24 | "{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?" 25 | ) 26 | end 27 | 28 | test :if_else do 29 | assert_result(" YES ", "{% if false %} NO {% else %} YES {% endif %}") 30 | assert_result(" YES ", "{% if true %} YES {% else %} NO {% endif %}") 31 | assert_result(" YES ", "{% if \"foo\" %} YES {% else %} NO {% endif %}") 32 | end 33 | 34 | test :if_boolean do 35 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => true}) 36 | end 37 | 38 | test :if_or do 39 | assert_result(" YES ", "{% if a or b %} YES {% endif %}", %{"a" => true, "b" => true}) 40 | assert_result(" YES ", "{% if a or b %} YES {% endif %}", %{"a" => true, "b" => false}) 41 | assert_result(" YES ", "{% if a or b %} YES {% endif %}", %{"a" => false, "b" => true}) 42 | assert_result("", "{% if a or b %} YES {% endif %}", %{"a" => false, "b" => false}) 43 | 44 | assert_result(" YES ", "{% if a or b or c %} YES {% endif %}", %{ 45 | "a" => false, 46 | "b" => false, 47 | "c" => true 48 | }) 49 | 50 | assert_result("", "{% if a or b or c %} YES {% endif %}", %{ 51 | "a" => false, 52 | "b" => false, 53 | "c" => false 54 | }) 55 | end 56 | 57 | test :if_or_with_operators do 58 | assert_result(" YES ", "{% if a == true or b == true %} YES {% endif %}", %{ 59 | "a" => true, 60 | "b" => true 61 | }) 62 | 63 | assert_result(" YES ", "{% if a == true or b == false %} YES {% endif %}", %{ 64 | "a" => true, 65 | "b" => true 66 | }) 67 | 68 | assert_result("", "{% if a == false or b == false %} YES {% endif %}", %{ 69 | "a" => true, 70 | "b" => true 71 | }) 72 | 73 | assert_result( 74 | " YES ", 75 | "{% if a == false and b == false and c == false %} YES {% endif %}", 76 | %{ 77 | "a" => false, 78 | "b" => false, 79 | "c" => false 80 | } 81 | ) 82 | 83 | template_body = """ 84 | {% if v1 == 1 and v2 == 2 and v3 == 3 %} Hello {% endif %} 85 | """ 86 | 87 | assert_result(" \n", template_body, %{"v1" => 2, "v2" => 2, "v3" => 3}) 88 | assert_result(" Hello \n", template_body, %{"v1" => 1, "v2" => 2, "v3" => 3}) 89 | end 90 | 91 | test :comparison_of_strings_containing_and_or_or do 92 | awful_markup = 93 | "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" 94 | 95 | assigns = %{ 96 | "a" => "and", 97 | "b" => "or", 98 | "c" => "foo and bar", 99 | "d" => "bar or baz", 100 | "e" => "foo", 101 | "foo" => true, 102 | "bar" => true 103 | } 104 | 105 | assert_result(" YES ", "{% if #{awful_markup} %} YES {% endif %}", assigns) 106 | end 107 | 108 | test :comparison_of_expressions_starting_with_and_or_or do 109 | assigns = %{"order" => %{"items_count" => 0}, "android" => %{"name" => "Roy"}} 110 | 111 | assert_result("YES", "{% if android.name == 'Roy' %}YES{% endif %}", assigns) 112 | assert_result("YES", "{% if order.items_count == 0 %}YES{% endif %}", assigns) 113 | end 114 | 115 | test :if_and do 116 | assert_result(" YES ", "{% if true and true %} YES {% endif %}") 117 | assert_result("", "{% if false and true %} YES {% endif %}") 118 | assert_result("", "{% if false and true %} YES {% endif %}") 119 | end 120 | 121 | test :hash_miss_generates_false do 122 | assert_result("", "{% if foo.bar %} NO {% endif %}", %{"foo" => %{}}) 123 | end 124 | 125 | test :if_from_variable do 126 | assert_result("", "{% if var %} NO {% endif %}", %{"var" => false}) 127 | assert_result("", "{% if var %} NO {% endif %}", %{"var" => nil}) 128 | assert_result("", "{% if foo.bar %} NO {% endif %}", %{"foo" => %{"bar" => false}}) 129 | assert_result("", "{% if foo.bar %} NO {% endif %}", %{"foo" => %{}}) 130 | 131 | assert_result("", "{% if foo.bar %} NO {% endif %}", %{"foo" => nil}) 132 | 133 | assert_result("", "{% if foo.bar %} NO {% endif %}", %{"foo" => true}) 134 | 135 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => "text"}) 136 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => true}) 137 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => 1}) 138 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => %{}}) 139 | assert_result(" YES ", "{% if var %} YES {% endif %}", %{"var" => %{}}) 140 | assert_result(" YES ", "{% if \"foo\" %} YES {% endif %}") 141 | assert_result(" YES ", "{% if foo.bar %} YES {% endif %}", %{"foo" => %{"bar" => true}}) 142 | assert_result(" YES ", "{% if foo.bar %} YES {% endif %}", %{"foo" => %{"bar" => "text"}}) 143 | assert_result(" YES ", "{% if foo.bar %} YES {% endif %}", %{"foo" => %{"bar" => 1}}) 144 | assert_result(" YES ", "{% if foo.bar %} YES {% endif %}", %{"foo" => %{"bar" => %{}}}) 145 | assert_result(" YES ", "{% if foo.bar %} YES {% endif %}", %{"foo" => %{"bar" => %{}}}) 146 | 147 | assert_result(" YES ", "{% if var %} NO {% else %} YES {% endif %}", %{"var" => false}) 148 | assert_result(" YES ", "{% if var %} NO {% else %} YES {% endif %}", %{"var" => nil}) 149 | assert_result(" YES ", "{% if var %} YES {% else %} NO {% endif %}", %{"var" => true}) 150 | assert_result(" YES ", "{% if \"foo\" %} YES {% else %} NO {% endif %}", %{"var" => "text"}) 151 | 152 | assert_result(" YES ", "{% if foo.bar %} NO {% else %} YES {% endif %}", %{ 153 | "foo" => %{"bar" => false} 154 | }) 155 | 156 | assert_result(" YES ", "{% if foo.bar %} YES {% else %} NO {% endif %}", %{ 157 | "foo" => %{"bar" => true} 158 | }) 159 | 160 | assert_result(" YES ", "{% if foo.bar %} YES {% else %} NO {% endif %}", %{ 161 | "foo" => %{"bar" => "text"} 162 | }) 163 | 164 | assert_result(" YES ", "{% if foo.bar %} NO {% else %} YES {% endif %}", %{ 165 | "foo" => %{"notbar" => true} 166 | }) 167 | 168 | assert_result(" YES ", "{% if foo.bar %} NO {% else %} YES {% endif %}", %{"foo" => %{}}) 169 | 170 | assert_result(" YES ", "{% if foo.bar %} NO {% else %} YES {% endif %}", %{ 171 | "notfoo" => %{"bar" => true} 172 | }) 173 | end 174 | 175 | test :nested_if do 176 | assert_result("", "{% if false %}{% if false %} NO {% endif %}{% endif %}") 177 | assert_result("", "{% if false %}{% if true %} NO {% endif %}{% endif %}") 178 | assert_result("", "{% if true %}{% if false %} NO {% endif %}{% endif %}") 179 | assert_result(" YES ", "{% if true %}{% if true %} YES {% endif %}{% endif %}") 180 | 181 | assert_result( 182 | " YES ", 183 | "{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}" 184 | ) 185 | 186 | assert_result( 187 | " YES ", 188 | "{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}" 189 | ) 190 | 191 | assert_result( 192 | " YES ", 193 | "{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}" 194 | ) 195 | end 196 | 197 | test :comparisons_on_null do 198 | assert_result("", "{% if null < 10 %} NO {% endif %}") 199 | assert_result("", "{% if null <= 10 %} NO {% endif %}") 200 | assert_result("", "{% if null >= 10 %} NO {% endif %}") 201 | assert_result("", "{% if null > 10 %} NO {% endif %}") 202 | 203 | assert_result("", "{% if 10 < null %} NO {% endif %}") 204 | assert_result("", "{% if 10 <= null %} NO {% endif %}") 205 | assert_result("", "{% if 10 >= null %} NO {% endif %}") 206 | assert_result("", "{% if 10 > null %} NO {% endif %}") 207 | end 208 | 209 | test :else_if do 210 | assert_result("0", "{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}") 211 | assert_result("1", "{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}") 212 | assert_result("2", "{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}") 213 | 214 | assert_result("elsif", "{% if false %}if{% elsif true %}elsif{% endif %}") 215 | end 216 | 217 | # test :syntax_error_no_variable do 218 | # assert_raise(SyntaxError){ assert_result("", "{% if jerry == 1 %}")} 219 | # end 220 | 221 | # test :syntax_error_no_expression do 222 | # assert_raise Liquid.SyntaxError, fn -> 223 | # assert_result("", "{% if %}") 224 | # end 225 | # end 226 | 227 | test :if_with_contains_condition do 228 | assert_result("yes", "{% if 'bob' contains 'o' %}yes{% endif %}") 229 | assert_result("no", "{% if 'bob' contains 'f' %}yes{% else %}no{% endif %}") 230 | 231 | assert_result( 232 | "yes", 233 | "{% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}" 234 | ) 235 | end 236 | 237 | defp assert_result(expected, markup, assigns \\ %{}) do 238 | t = Template.parse(markup) 239 | {:ok, rendered, _} = Template.render(t, assigns) 240 | assert rendered == expected 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /test/tags/include_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule TestFileSystem do 4 | def read_template_file(_root, template_path, _context) do 5 | case template_path do 6 | "product" -> 7 | {:ok, "Product: {{ product.title }} "} 8 | 9 | "locale_variables" -> 10 | {:ok, "Locale: {{echo1}} {{echo2}}"} 11 | 12 | "variant" -> 13 | {:ok, "Variant: {{ variant.title }}"} 14 | 15 | "nested_template" -> 16 | {:ok, "{% include 'header' %} {% include 'body' %} {% include 'footer' %}"} 17 | 18 | "body" -> 19 | {:ok, "body {% include 'body_detail' %}"} 20 | 21 | "nested_product_template" -> 22 | {:ok, "Product: {{ nested_product_template.title }} {%include 'details'%} "} 23 | 24 | "recursively_nested_template" -> 25 | {:ok, "-{% include 'recursively_nested_template' %}"} 26 | 27 | "pick_a_source" -> 28 | {:ok, "from TestFileSystem"} 29 | 30 | _ -> 31 | {:ok, template_path} 32 | end 33 | end 34 | end 35 | 36 | defmodule OtherFileSystem do 37 | def read_template_file(_root, _template_path, _context) do 38 | {:ok, "from OtherFileSystem"} 39 | end 40 | end 41 | 42 | defmodule IncludeTagTest do 43 | use ExUnit.Case 44 | 45 | alias Liquid.Template, as: Template 46 | alias Liquid.Context, as: Context 47 | 48 | setup_all do 49 | Liquid.start() 50 | Liquid.FileSystem.register(TestFileSystem) 51 | on_exit(fn -> Liquid.stop() end) 52 | :ok 53 | end 54 | 55 | test :include_tag_looks_for_file_system_in_registers_first do 56 | assert_result("from OtherFileSystem", "{% include 'pick_a_source' %}", %Context{ 57 | registers: %{file_system: {OtherFileSystem, ""}} 58 | }) 59 | end 60 | 61 | test :include_tag_with do 62 | assert_result("Product: Draft 151cm ", "{% include 'product' with products[0] %}", %{ 63 | "products" => [%{"title" => "Draft 151cm"}, %{"title" => "Element 155cm"}] 64 | }) 65 | end 66 | 67 | test :include_tag_with_default_name do 68 | assert_result("Product: Draft 151cm ", "{% include 'product' %}", %{ 69 | "product" => %{"title" => "Draft 151cm"} 70 | }) 71 | end 72 | 73 | test :include_tag_for do 74 | assert_result( 75 | "Product: Draft 151cm Product: Element 155cm ", 76 | "{% include 'product' for products %}", 77 | %{"products" => [%{"title" => "Draft 151cm"}, %{"title" => "Element 155cm"}]} 78 | ) 79 | end 80 | 81 | test :include_tag_with_local_variables do 82 | assert_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}") 83 | end 84 | 85 | test :include_tag_with_multiple_local_variables do 86 | assert_result( 87 | "Locale: test123 test321", 88 | "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}" 89 | ) 90 | end 91 | 92 | test :include_tag_with_multiple_local_variables_from_context do 93 | assert_result( 94 | "Locale: test123 test321", 95 | "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", 96 | %{"echo1" => "test123", "more_echos" => %{"echo2" => "test321"}} 97 | ) 98 | end 99 | 100 | test :nested_include_tag do 101 | assert_result("body body_detail", "{% include 'body' %}") 102 | assert_result("header body body_detail footer", "{% include 'nested_template' %}") 103 | end 104 | 105 | test :nested_include_with_variable do 106 | assert_result( 107 | "Product: Draft 151cm details ", 108 | "{% include 'nested_product_template' with product %}", 109 | %{"product" => %{"title" => "Draft 151cm"}} 110 | ) 111 | 112 | assert_result( 113 | "Product: Draft 151cm details Product: Element 155cm details ", 114 | "{% include 'nested_product_template' for products %}", 115 | %{"products" => [%{"title" => "Draft 151cm"}, %{"title" => "Element 155cm"}]} 116 | ) 117 | end 118 | 119 | # test :recursively_included_template_does_not_produce_endless_loop do 120 | # infinite_file_system = defmodule InfiniteFileSystem do 121 | # def read_template_file(root, template_path, context) do 122 | # "-{% include 'loop' %}" 123 | # end 124 | # end 125 | # Liquid.FileSystem.register infinite_file_system 126 | # t = Template.parse("{% include 'loop' %}") 127 | # { :error, _ } = Template.render(t) 128 | # end 129 | 130 | # test :backwards_compatability_support_for_overridden_read_template_file do 131 | # infinite_file_system = defmodule InfiniteFileSystem do 132 | # def read_template_file(root, template_path, context) do 133 | # "- hi mom" 134 | # end 135 | # end 136 | # Liquid.FileSystem.register infinite_file_system 137 | # t = Template.parse("{% include 'hi_mom' %}") 138 | # { :ok, _ } = Template.render(t) 139 | # end 140 | 141 | # test :dynamically_choosen_template do 142 | # assert_result "Test123", "{% include template %}", [template: "Test123"] 143 | # assert_result "Test321", "{% include template %}", [template: "Test321"] 144 | 145 | # assert_result "Product: Draft 151cm ", 146 | # "{% include template for product %}", 147 | # [template: "product", product: [title: "Draft 151cm"]] 148 | # end 149 | 150 | defp assert_result(expected, markup), do: assert_result(expected, markup, %Liquid.Context{}) 151 | 152 | defp assert_result(expected, markup, %Liquid.Context{} = context) do 153 | t = Template.parse(markup) 154 | {:ok, rendered, _context} = Template.render(t, context) 155 | assert expected == rendered 156 | end 157 | 158 | defp assert_result(expected, markup, assigns) do 159 | context = %Liquid.Context{assigns: assigns} 160 | assert_result(expected, markup, context) 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/tags/increment_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.IncrementTest do 4 | use ExUnit.Case 5 | alias Liquid.Template 6 | 7 | setup_all do 8 | Liquid.start() 9 | :ok 10 | end 11 | 12 | test :test_inc do 13 | assert_template_result("0", "{%increment port %}", %{}) 14 | assert_template_result("0 1", "{%increment port %} {%increment port%}", %{}) 15 | 16 | assert_template_result( 17 | "0 0 1 2 1", 18 | "{%increment port %} {%increment starboard%} {%increment port %} {%increment port%} {%increment starboard %}", 19 | %{} 20 | ) 21 | end 22 | 23 | test :test_dec do 24 | assert_template_result("9", "{%decrement port %}", %{"port" => 10}) 25 | assert_template_result("-1 -2", "{%decrement port %} {%decrement port%}", %{}) 26 | 27 | assert_template_result( 28 | "1 5 2 2 5", 29 | "{%increment port %} {%increment starboard%} {%increment port %} {%decrement port%} {%decrement starboard %}", 30 | %{"port" => 1, "starboard" => 5} 31 | ) 32 | end 33 | 34 | defp assert_template_result(expected, markup, assigns) do 35 | assert_result(expected, markup, assigns) 36 | end 37 | 38 | defp assert_result(expected, markup, assigns) do 39 | template = Template.parse(markup) 40 | {:ok, result, _} = Template.render(template, assigns) 41 | assert result == expected 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/tags/raw_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.RawTest do 4 | use ExUnit.Case 5 | 6 | alias Liquid.Template, as: Template 7 | 8 | setup_all do 9 | Liquid.start() 10 | :ok 11 | end 12 | 13 | test :test_tag_in_raw do 14 | assert_template_result( 15 | "{% comment %} test {% endcomment %}", 16 | "{% raw %}{% comment %} test {% endcomment %}{% endraw %}" 17 | ) 18 | end 19 | 20 | test :test_output_in_raw do 21 | assert_template_result("{{ test }}", "{% raw %}{{ test }}{% endraw %}") 22 | end 23 | 24 | test :test_open_tag_in_raw do 25 | assert_template_result(" Foobar {% invalid ", "{% raw %} Foobar {% invalid {% endraw %}") 26 | assert_template_result(" Foobar invalid %} ", "{% raw %} Foobar invalid %} {% endraw %}") 27 | assert_template_result(" Foobar {{ invalid ", "{% raw %} Foobar {{ invalid {% endraw %}") 28 | assert_template_result(" Foobar invalid }} ", "{% raw %} Foobar invalid }} {% endraw %}") 29 | 30 | assert_template_result( 31 | " Foobar {% invalid {% {% endraw ", 32 | "{% raw %} Foobar {% invalid {% {% endraw {% endraw %}" 33 | ) 34 | 35 | assert_template_result(" Foobar {% {% {% ", "{% raw %} Foobar {% {% {% {% endraw %}") 36 | 37 | assert_template_result( 38 | " test {% raw %} {% endraw %}", 39 | "{% raw %} test {% raw %} {% {% endraw %}endraw %}" 40 | ) 41 | 42 | assert_template_result( 43 | " Foobar {{ invalid 1", 44 | "{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}" 45 | ) 46 | end 47 | 48 | # test :test_invalid_raw do 49 | # assert_match_syntax_error ~r/tag was never closed/, "{% raw %} foo" 50 | # assert_match_syntax_error ~r/Valid syntax/, "{% raw } foo {% endraw %}" 51 | # assert_match_syntax_error ~r/Valid syntax/, "{% raw } foo %}{% endraw %}" 52 | # end 53 | 54 | defp assert_template_result(expected, markup) do 55 | assert_result(expected, markup, %{}) 56 | end 57 | 58 | defp assert_result(expected, markup, assigns) do 59 | template = Template.parse(markup) 60 | 61 | {:ok, result, _} = Template.render(template, assigns) 62 | assert result == expected 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/tags/statements_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.StatementsTest do 4 | use ExUnit.Case 5 | alias Liquid.Template 6 | 7 | setup_all do 8 | Liquid.start() 9 | :ok 10 | end 11 | 12 | test :test_true_eql_true do 13 | text = " {% if true == true %} true {% else %} false {% endif %} " 14 | assert_template_result(" true ", text) 15 | end 16 | 17 | test :test_true_not_eql_true do 18 | text = " {% if true != true %} true {% else %} false {% endif %} " 19 | assert_template_result(" false ", text) 20 | end 21 | 22 | test :test_true_lq_true do 23 | text = " {% if 0 > 0 %} true {% else %} false {% endif %} " 24 | assert_template_result(" false ", text) 25 | end 26 | 27 | test :test_one_lq_zero do 28 | text = " {% if 1 > 0 %} true {% else %} false {% endif %} " 29 | assert_template_result(" true ", text) 30 | end 31 | 32 | test :test_zero_lq_one do 33 | text = " {% if 0 < 1 %} true {% else %} false {% endif %} " 34 | assert_template_result(" true ", text) 35 | end 36 | 37 | test :test_zero_lq_or_equal_one do 38 | text = " {% if 0 <= 0 %} true {% else %} false {% endif %} " 39 | assert_template_result(" true ", text) 40 | end 41 | 42 | test :test_zero_lq_or_equal_one_involving_nil do 43 | text = " {% if null <= 0 %} true {% else %} false {% endif %} " 44 | assert_template_result(" false ", text) 45 | 46 | text = " {% if 0 <= null %} true {% else %} false {% endif %} " 47 | assert_template_result(" false ", text) 48 | end 49 | 50 | test :test_zero_lqq_or_equal_one do 51 | text = " {% if 0 >= 0 %} true {% else %} false {% endif %} " 52 | assert_template_result(" true ", text) 53 | end 54 | 55 | test :test_strings do 56 | text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " 57 | assert_template_result(" true ", text) 58 | end 59 | 60 | test :test_strings_not_equal do 61 | text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " 62 | assert_template_result(" false ", text) 63 | end 64 | 65 | test :test_var_strings_equal do 66 | text = " {% if var == \"hello there!\" %} true {% else %} false {% endif %} " 67 | assert_template_result(" true ", text, %{"var" => "hello there!"}) 68 | end 69 | 70 | test :test_var_strings_are_not_equal do 71 | text = " {% if \"hello there!\" == var %} true {% else %} false {% endif %} " 72 | assert_template_result(" true ", text, %{"var" => "hello there!"}) 73 | end 74 | 75 | test :test_var_and_long_string_are_equal do 76 | text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " 77 | assert_template_result(" true ", text, %{"var" => "hello there!"}) 78 | end 79 | 80 | test :test_var_and_long_string_are_equal_backwards do 81 | text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " 82 | assert_template_result(" true ", text, %{"var" => "hello there!"}) 83 | end 84 | 85 | test :test_is_collection_empty do 86 | text = " {% if array == empty %} true {% else %} false {% endif %} " 87 | assert_template_result(" true ", text, %{"array" => []}) 88 | end 89 | 90 | test :test_is_not_collection_empty do 91 | text = " {% if array == empty %} true {% else %} false {% endif %} " 92 | assert_template_result(" false ", text, %{"array" => [1, 2, 3]}) 93 | end 94 | 95 | test :test_nil do 96 | text = " {% if var == nil %} true {% else %} false {% endif %} " 97 | assert_template_result(" true ", text, %{"var" => nil}) 98 | 99 | text = " {% if var == null %} true {% else %} false {% endif %} " 100 | assert_template_result(" true ", text, %{"var" => nil}) 101 | end 102 | 103 | test :test_not_nil do 104 | text = " {% if var != nil %} true {% else %} false {% endif %} " 105 | assert_template_result(" true ", text, %{"var" => 1}) 106 | 107 | text = " {% if var != null %} true {% else %} false {% endif %} " 108 | assert_template_result(" true ", text, %{"var" => 1}) 109 | end 110 | 111 | defp assert_template_result(expected, markup) do 112 | assert_result(expected, markup, %{}) 113 | end 114 | 115 | defp assert_template_result(expected, markup, assigns) do 116 | assert_result(expected, markup, assigns) 117 | end 118 | 119 | defp assert_result(expected, markup, assigns) do 120 | template = Template.parse(markup) 121 | {:ok, result, _} = Template.render(template, assigns) 122 | assert result == expected 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/tags/table_row_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.TableRowTest do 4 | use ExUnit.Case 5 | 6 | test :test_table_row do 7 | assert_template_result( 8 | "a\n 1 2 3 \n 4 5 6 \n", 9 | "a{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}", 10 | %{"numbers" => [1, 2, 3, 4, 5, 6]} 11 | ) 12 | 13 | assert_template_result( 14 | "a\n\n", 15 | "a{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}", 16 | %{"numbers" => []} 17 | ) 18 | end 19 | 20 | test "tablerow class ids not related to numbers" do 21 | assert_template_result( 22 | "\n 2 3 \n", 23 | "{% tablerow n in (2..3) cols:3%} {{n}} {% endtablerow %}" 24 | ) 25 | end 26 | 27 | test :test_table_row_with_different_cols do 28 | assert_template_result( 29 | "\n 1 2 3 4 5 \n 6 \n", 30 | "{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}", 31 | %{"numbers" => [1, 2, 3, 4, 5, 6]} 32 | ) 33 | end 34 | 35 | test :test_table_col_counter do 36 | assert_template_result( 37 | "\n12\n12\n12\n", 38 | "{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}", 39 | %{"numbers" => [1, 2, 3, 4, 5, 6]} 40 | ) 41 | end 42 | 43 | test :test_quoted_fragment do 44 | assert_template_result( 45 | "\n 1 2 3 \n 4 5 6 \n", 46 | "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", 47 | %{"collections" => %{"frontpage" => [1, 2, 3, 4, 5, 6]}} 48 | ) 49 | 50 | assert_template_result( 51 | "\n 1 2 3 \n 4 5 6 \n", 52 | "{% tablerow n in collections[\"frontpage\"] cols:3%} {{n}} {% endtablerow %}", 53 | %{"collections" => %{"frontpage" => [1, 2, 3, 4, 5, 6]}} 54 | ) 55 | end 56 | 57 | # test :test_enumerable_drop do 58 | # assert_template_result("\n 1 2 3 \n 4 5 6 \n", 59 | # "{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}", 60 | # %{"numbers"=> %ArrayDrop{"list"=>[1, 2, 3, 4, 5, 6]}}) 61 | # end 62 | 63 | test :test_offset_and_limit do 64 | assert_template_result( 65 | "\n 1 2 3 \n 4 5 6 \n", 66 | "{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}", 67 | %{"numbers" => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} 68 | ) 69 | end 70 | 71 | test :test_blank_string_not_iterable do 72 | assert_template_result( 73 | "\n\n", 74 | "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 75 | %{"characters" => ""} 76 | ) 77 | end 78 | 79 | test "continue in tablerow" do 80 | assert_template_result( 81 | "\n123\n", 82 | "{% tablerow i in (1..7) %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endtablerow %}" 83 | ) 84 | end 85 | 86 | defp assert_template_result(expected, markup, assigns \\ %{}) do 87 | assert_result(expected, markup, assigns) 88 | end 89 | 90 | defp assert_result(expected, markup, assigns) do 91 | template = Liquid.Template.parse(markup) 92 | {:ok, result, _} = Liquid.Template.render(template, assigns) 93 | assert result == expected 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/tags/unless_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../../test_helper.exs", __ENV__.file) 2 | 3 | defmodule Liquid.UnlessTest do 4 | use ExUnit.Case 5 | alias Liquid.Template 6 | 7 | setup_all do 8 | Liquid.start() 9 | :ok 10 | end 11 | 12 | test :test_unless do 13 | assert_template_result( 14 | " ", 15 | " {% unless true %} this text should not go into the output {% endunless %} " 16 | ) 17 | 18 | assert_template_result( 19 | " this text should go into the output ", 20 | " {% unless false %} this text should go into the output {% endunless %} " 21 | ) 22 | 23 | assert_template_result( 24 | " you rock ?", 25 | "{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?" 26 | ) 27 | end 28 | 29 | test :test_unless_else do 30 | assert_template_result(" YES ", "{% unless true %} NO {% else %} YES {% endunless %}") 31 | assert_template_result(" YES ", "{% unless false %} YES {% else %} NO {% endunless %}") 32 | assert_template_result(" YES ", "{% unless \"foo\" %} NO {% else %} YES {% endunless %}") 33 | end 34 | 35 | test :test_unless_in_loop do 36 | assert_template_result( 37 | "23", 38 | "{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}", 39 | %{"choices" => [1, nil, false]} 40 | ) 41 | end 42 | 43 | test :test_unless_else_in_loop do 44 | assert_template_result( 45 | " TRUE 2 3 ", 46 | "{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}", 47 | %{"choices" => [1, nil, false]} 48 | ) 49 | end 50 | 51 | defp assert_template_result(expected, markup) do 52 | assert_result(expected, markup, %{}) 53 | end 54 | 55 | defp assert_template_result(expected, markup, assigns) do 56 | assert_result(expected, markup, assigns) 57 | end 58 | 59 | defp assert_result(expected, markup, assigns) do 60 | template = Template.parse(markup) 61 | {:ok, result, _} = Template.render(template, assigns) 62 | assert result == expected 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/templates/complex/01/input.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% for collection in collections %} 3 | {% for product in collection.products %} 4 | {% for variant in product.variants %} 5 |

{{ collection.title | upcase }}/br{{ collection.title }}

6 | {% if collection.description.size > 0 %} 7 |
{{ collection.description }}
8 | {% endif %} 9 |
    10 |
  • 11 |
    12 |
    13 |
    14 |
    15 |

    {{ product.title }}

    16 |

    {{ product.description | truncatewords: 15 }}

    17 |
    18 | {{ product.title | escape }} 19 |
    20 |
    21 |

    22 | 23 | 24 | 25 | 26 |

    27 | 28 |

    29 | View Details 30 | 31 | {% if product.compare_at_price %} 32 | {% if product.price_min != product.compare_at_price %} 33 | {{ product.compare_at_price }} - 34 | {% endif %} 35 | {% endif %} 36 | 37 | {{ product.price_min }} 38 | 39 | 40 |

    41 |
    42 |
    43 |
    44 |
  • 45 |
46 |

47 | {{ product.price | plus: 1000}} 48 | {{ product.price | divide_by: 1000}} 49 | {{ product.price | minus: 1000}} 50 | {% assign all_products = collection.products | map: "price" %} 51 | {% for item in all_products %} 52 | {{ item }} 53 | {{ all_products | sort | join: ", " }} 54 | {% case product.vendor%} 55 | {% when 'Nikon' %} 56 | This is a camera 57 | {% when 'Stormtech' %} 58 | This is a Sweater 59 | {% else %} 60 | This is not a camera nor a Sweater 61 | {% endcase %} 62 | {% endfor %} 63 |

64 | {% endfor %} 65 | {% endfor %} 66 | {% endfor %} 67 | {% assign fruits = "apples, oranges, peaches" | split: ", " %} 68 | {% assign vegetables = "carrots, turnips, potatoes" | split: ", " %} 69 | {% for item in fruits %} 70 | - {{ item }} 71 | {% endfor %} 72 | {% for item in vegetables %} 73 | - {{ item }} 74 | {% endfor %} 75 |
76 | -------------------------------------------------------------------------------- /test/templates/liquid.rb: -------------------------------------------------------------------------------- 1 | require 'liquid' 2 | require 'json' 3 | data = File.read(ARGV[1]) 4 | template = File.read(ARGV[0]) 5 | hash = JSON.parse(data) 6 | # puts Liquid::Template.parse(template).render(hash) 7 | File.write ARGV[2], Liquid::Template.parse(template).render(hash) 8 | -------------------------------------------------------------------------------- /test/templates/medium/01/input.liquid: -------------------------------------------------------------------------------- 1 |
    2 | {% for collection in collections%} 3 | {% for product in collection.products %} 4 |
  • 5 |
    6 |
    7 |
    8 |
    9 |

    {{product.title}}

    10 |

    {{ product.description | strip_html | truncatewords: 35 }}

    11 |

    {% if product.price_varies %} -

    Varies the price<\p>{% endif %}

    12 |
    13 |
  • 14 | {% endfor %} 15 | {% endfor %} 16 |
17 | -------------------------------------------------------------------------------- /test/templates/medium/01/output.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
  • 5 |
    6 |
    7 |
    8 |
    9 |

    Shopify Shirt

    10 |

    High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua....

    11 |

    12 |
    13 |
  • 14 | 15 |
  • 16 |
    17 |
    18 |
    19 |
    20 |

    Hooded Sweater

    21 |

    Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim...

    22 |

    23 |
    24 |
  • 25 | 26 |
  • 27 |
    28 |
    29 |
    30 |
    31 |

    D3 Digital SLR Camera

    32 |

    Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO. Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's...

    33 |

    -

    Varies the price<\p>

    34 |
    35 |
  • 36 | 37 | 38 | 39 |
  • 40 |
    41 |
    42 |
    43 |
    44 |

    Arbor Draft

    45 |

    The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a...

    46 |

    -

    Varies the price<\p>

    47 |
    48 |
  • 49 | 50 |
  • 51 |
    52 |
    53 |
    54 |
    55 |

    Arbor Element

    56 |

    The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience....

    57 |

    58 |
    59 |
  • 60 | 61 | 62 | 63 |
  • 64 |
    65 |
    66 |
    67 |
    68 |

    Arbor Draft

    69 |

    The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a...

    70 |

    -

    Varies the price<\p>

    71 |
    72 |
  • 73 | 74 |
  • 75 |
    76 |
    77 |
    78 |
    79 |

    Arbor Element

    80 |

    The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience....

    81 |

    82 |
    83 |
  • 84 | 85 |
  • 86 |
    87 |
    88 |
    89 |
    90 |

    Comic ~ Pastel

    91 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    92 |

    -

    Varies the price<\p>

    93 |
    94 |
  • 95 | 96 |
  • 97 |
    98 |
    99 |
    100 |
    101 |

    Comic ~ Orange

    102 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    103 |

    104 |
    105 |
  • 106 | 107 | 108 | 109 |
  • 110 |
    111 |
    112 |
    113 |
    114 |

    Arbor Draft

    115 |

    The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a...

    116 |

    -

    Varies the price<\p>

    117 |
    118 |
  • 119 | 120 | 121 | 122 |
  • 123 |
    124 |
    125 |
    126 |
    127 |

    Arbor Draft

    128 |

    The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a...

    129 |

    -

    Varies the price<\p>

    130 |
    131 |
  • 132 | 133 |
  • 134 |
    135 |
    136 |
    137 |
    138 |

    Arbor Element

    139 |

    The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience....

    140 |

    141 |
    142 |
  • 143 | 144 |
  • 145 |
    146 |
    147 |
    148 |
    149 |

    Comic ~ Pastel

    150 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    151 |

    -

    Varies the price<\p>

    152 |
    153 |
  • 154 | 155 |
  • 156 |
    157 |
    158 |
    159 |
    160 |

    Comic ~ Orange

    161 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    162 |

    163 |
    164 |
  • 165 | 166 | 167 | 168 |
  • 169 |
    170 |
    171 |
    172 |
    173 |

    Shopify Shirt

    174 |

    High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua....

    175 |

    176 |
    177 |
  • 178 | 179 |
  • 180 |
    181 |
    182 |
    183 |
    184 |

    Hooded Sweater

    185 |

    Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim...

    186 |

    187 |
    188 |
  • 189 | 190 |
  • 191 |
    192 |
    193 |
    194 |
    195 |

    D3 Digital SLR Camera

    196 |

    Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO. Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's...

    197 |

    -

    Varies the price<\p>

    198 |
    199 |
  • 200 | 201 |
  • 202 |
    203 |
    204 |
    205 |
    206 |

    Superbike 1198 S

    207 |

    ‘S’ PERFORMANCE Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range...

    208 |

    209 |
    210 |
  • 211 | 212 |
  • 213 |
    214 |
    215 |
    216 |
    217 |

    Arbor Draft

    218 |

    The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a...

    219 |

    -

    Varies the price<\p>

    220 |
    221 |
  • 222 | 223 |
  • 224 |
    225 |
    226 |
    227 |
    228 |

    Arbor Element

    229 |

    The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience....

    230 |

    231 |
    232 |
  • 233 | 234 |
  • 235 |
    236 |
    237 |
    238 |
    239 |

    Comic ~ Pastel

    240 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    241 |

    -

    Varies the price<\p>

    242 |
    243 |
  • 244 | 245 |
  • 246 |
    247 |
    248 |
    249 |
    250 |

    Comic ~ Orange

    251 |

    2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or...

    252 |

    253 |
    254 |
  • 255 | 256 |
  • 257 |
    258 |
    259 |
    260 |
    261 |

    Burton Boots

    262 |

    The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.

    263 |

    264 |
    265 |
  • 266 | 267 | 268 |
269 | -------------------------------------------------------------------------------- /test/templates/medium/02/input.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% for collection in collections%} 3 | {% for product in collection.products %} 4 |

{{ collection.title }} {{ product.title }}

5 |

Product Tags: 6 | {% for tag in product.tags %} 7 | {{ tag }} | 8 | {% endfor %} 9 |

10 |
11 |
12 |

{{ product.title }}

13 |
14 |

{{ product.description }}

15 |
16 | {% if product.available %} 17 |
18 |

Product Options:

19 | 24 |
25 | 26 |
27 |
28 | {% else %} 29 |

Sold out!

30 |

Sorry, we're all out of this product. Check back often and order when it returns

31 | {% endif %} 32 |
33 |
34 | {% for image in product.images %} 35 | {% if forloop.first %} 36 |
37 | {{product.title | escape }} 38 |
39 | {% endif %} 40 | {% endfor %} 41 |
    42 | {% for image in product.images %} 43 | {% if forloop.first %} 44 | {% else %} 45 |
  • 46 | 47 | {{product.title | escape }} 48 | 49 |
  • 50 | {% endif %} 51 | {% endfor %} 52 |
53 |
54 |
55 | 77 | {% endfor %} 78 | {% endfor %} 79 |
80 | 81 | -------------------------------------------------------------------------------- /test/templates/medium/03/input.liquid: -------------------------------------------------------------------------------- 1 |
2 | {%for blog in blogs%} 3 | {%for article in blog.articles%} 4 |
5 |
6 |

{{article.title}}

7 |
8 |
{{ article.created_at | date: "%b %d" }}
9 | {{ article.content }} 10 |
11 | 12 | {% if blog.comments_enabled %} 13 |
14 |

Comments

15 | 16 |
    17 | {% for comment in article.comments %} 18 |
  • 19 |
    20 | {{ comment.content }} 21 |
    22 | 23 |
    24 | Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} 25 |
    26 |
  • 27 | {% endfor %} 28 |
29 | 30 |
31 |

Leave a comment

32 | {% if form.posted_successfully? %} 33 | {% if blog.moderated %} 34 |
35 | Successfully posted your comment.
36 | It will have to be approved by the blog owner first before showing up. 37 |
38 | {% else %} 39 |
Successfully posted your comment.
40 | {% endif %} 41 | {% endif %} 42 | {% if form.errors %} 43 |
Not all the fields have been filled out correctly!
44 | {% endif %} 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {% if blog.moderated %} 54 |

comments have to be approved before showing up

55 | {% endif %} 56 | 57 |
58 | 59 |
60 | {% endif %} 61 | 62 |
63 |
64 | {%endfor%} 65 | {%endfor%} 66 |
67 | -------------------------------------------------------------------------------- /test/templates/medium/03/output.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |

Welcome to the new Foo Shop

7 |
8 |
Apr 04
9 |

Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.

To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.

The text you see here is an article. To edit this article, create new articles or create new pages you can go to the Blogs & Pages tab of the administration menu.

The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the Products Tab in of the administration menu.

While you're looking around be sure to check out the Collections and Navigations tabs and soon you will be well on your way to populating your site.

And of course don't forget to browse the theme gallery to pick a new look for your shop!

Shopify is in beta
If you would like to make comments or suggestions please visit us in the Shopify Forums or drop us an email.

10 |
11 | 12 | 13 |
14 |

Comments

15 | 16 |
    17 | 18 |
19 | 20 |
21 |

Leave a comment

22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |

comments have to be approved before showing up

34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 |
42 |
43 | 44 |
45 |
46 |

Breaking News: Restock on all sales products

47 |
48 |
Apr 04
49 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 50 |
51 | 52 | 53 |
54 |

Comments

55 | 56 |
    57 | 58 |
59 | 60 |
61 |

Leave a comment

62 | 63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 |

comments have to be approved before showing up

74 | 75 | 76 |
77 | 78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 |
87 |
88 |

One thing you probably did not know yet...

89 |
90 |
Apr 04
91 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 92 |
93 | 94 | 95 |
96 |

Comments

97 | 98 |
    99 | 100 |
  • 101 |
    102 | Wow...great article man. 103 |
    104 | 105 |
    106 | Posted by John Smith on January 01, 2009 107 |
    108 |
  • 109 | 110 |
  • 111 |
    112 | I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks! 113 |
    114 | 115 |
    116 | Posted by John Jones on March 01, 2009 117 |
    118 |
  • 119 | 120 |
121 | 122 |
123 |

Leave a comment

124 | 125 | 126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | 135 |

comments have to be approved before showing up

136 | 137 | 138 |
139 | 140 |
141 | 142 | 143 |
144 |
145 | 146 |
147 |
148 |

Fascinating

149 |
150 |
Apr 06
151 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 152 |
153 | 154 | 155 |
156 |

Comments

157 | 158 |
    159 | 160 |
161 | 162 |
163 |

Leave a comment

164 | 165 | 166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | 175 |

comments have to be approved before showing up

176 | 177 | 178 |
179 | 180 |
181 | 182 | 183 |
184 |
185 | 186 | 187 | 188 |
189 |
190 |

One thing you probably did not know yet...

191 |
192 |
Apr 04
193 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 194 |
195 | 196 | 197 |
198 |

Comments

199 | 200 |
    201 | 202 |
203 | 204 |
205 |

Leave a comment

206 | 207 | 208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | 217 |

comments have to be approved before showing up

218 | 219 | 220 |
221 | 222 |
223 | 224 | 225 |
226 |
227 | 228 |
229 |
230 |

Fascinating

231 |
232 |
Apr 06
233 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 234 |
235 | 236 | 237 |
238 |

Comments

239 | 240 |
    241 | 242 |
243 | 244 |
245 |

Leave a comment

246 | 247 | 248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | 257 |

comments have to be approved before showing up

258 | 259 | 260 |
261 | 262 |
263 | 264 | 265 |
266 |
267 | 268 | 269 |
270 | -------------------------------------------------------------------------------- /test/templates/simple/01/input.liquid: -------------------------------------------------------------------------------- 1 | {% for page in pages %} 2 | {% for blog in blogs %} 3 |
4 |

{{page.title}}

5 | {% for article in blog.articles %} 6 |

7 | {{ article.created_at | date: "%d %b" }} 8 | {{ article.title }} 9 |

10 | {{ article.content }} 11 | {% if blog.comments_enabled %} 12 |

{{ article.comments_count }} comments

13 | {% endif %} 14 | {% endfor %} 15 |
16 | {% endfor %} 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /test/templates/simple/02/input.liquid: -------------------------------------------------------------------------------- 1 | {% for product in products %} 2 |
3 |
4 |
5 |
6 | {% for image in product.images %} 7 | {% if forloop.first %} 8 | 9 | {{product.title | escape }} 10 | 11 | {% else %} 12 | 13 | {{product.title | escape }} 14 | 15 | {% endif %} 16 | {% endfor %} 17 |
18 |
19 |
20 |

{{ product.title }}

21 |
    22 |
  • Vendor: html filter
  • 23 |
  • Type: Type filter
  • 24 |
25 | Money filter{% if product.price_varies %} -

Prices varies money filter

{% endif %}
26 |
27 |
28 | 33 |
34 |
35 |
36 |
37 |
38 | {{ product.description }} 39 |
40 |
41 | {% endfor %} 42 | -------------------------------------------------------------------------------- /test/templates/simple/03/input.liquid: -------------------------------------------------------------------------------- 1 | {% for page in pages %} 2 |
3 |

{{page.title}}

4 | {% for blog in blogs %} 5 | {% for article in blog.articles %} 6 |
7 |
8 |

9 | {{ article.title }} 10 |

11 |

Posted on {{ article.created_at | date: "%B %d, %y" }} by {{ article.author }}.

12 |
13 | 14 |
15 | {{ article.content | strip_html | truncate: 250 }} 16 |
17 | 18 | {% if blog.comments_enabled %} 19 |

Comentarios

20 | {% endif %} 21 |
22 | {% endfor %} 23 | {% endfor %} 24 |
25 | What is this 26 |
27 |
28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: [:skip]) 2 | 3 | defmodule Liquid.Helpers do 4 | def render(text, data \\ %{}) do 5 | text |> Liquid.Template.parse() |> Liquid.Template.render(data) |> elem(1) 6 | end 7 | end 8 | --------------------------------------------------------------------------------