├── test ├── test_helper.exs ├── fixtures │ ├── simple │ │ ├── foo.html.haml │ │ ├── edit.html.haml │ │ ├── new.html.haml │ │ ├── show.html.haml │ │ ├── index.html.haml │ │ └── layouts │ │ │ └── application.html.haml │ ├── list_comprehension_example.haml │ └── simple_example.haml ├── calliope_test.exs └── calliope │ ├── engine_test.exs │ ├── safe_test.exs │ ├── render_test.exs │ ├── integration_test.exs │ ├── tokenizer_test.exs │ ├── parser_test.exs │ └── compiler_test.exs ├── .gitignore ├── .travis.yml ├── lib ├── calliope.ex └── calliope │ ├── render.ex │ ├── exceptions.ex │ ├── safe.ex │ ├── tokenizer.ex │ ├── engine.ex │ ├── parser.ex │ └── compile.ex ├── mix.exs ├── README.md └── LICENSE /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/foo.html.haml: -------------------------------------------------------------------------------- 1 | This is foo 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/edit.html.haml: -------------------------------------------------------------------------------- 1 | This is the edit page 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/new.html.haml: -------------------------------------------------------------------------------- 1 | This is the new page 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/show.html.haml: -------------------------------------------------------------------------------- 1 | This is the show page 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1= title 2 | Index Page 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /ebin 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.0 4 | otp_release: 5 | - 17.0 6 | -------------------------------------------------------------------------------- /test/calliope_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeTest do 2 | use ExUnit.Case 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/simple/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | %h1 The Application Layout 2 | = yield 3 | -------------------------------------------------------------------------------- /test/fixtures/list_comprehension_example.haml: -------------------------------------------------------------------------------- 1 | - lc { id, content } inlist posts do 2 | %div 3 | = content 4 | -------------------------------------------------------------------------------- /lib/calliope.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope do 2 | use Calliope.Render 3 | 4 | defmacro __using__([]) do 5 | quote do 6 | import unquote __MODULE__ 7 | 8 | use Calliope.Render 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | Code.ensure_loaded?(Hex) and Hex.start 2 | 3 | defmodule Calliope.Mixfile do 4 | use Mix.Project 5 | 6 | def project do 7 | [ app: :calliope, 8 | version: "0.4.0", 9 | elixir: ">= 1.0.0", 10 | deps: [], 11 | package: [ 12 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 13 | contributors: ["Johnny Winn", "Stephen Pallen"], 14 | licenses: ["Apache 2.0"], 15 | links: %{ "Github" => "https://github.com/nurugger07/calliope" } 16 | ], 17 | description: """ 18 | An Elixir library for parsing haml templates. 19 | """ 20 | ] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/calliope/render.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Render do 2 | import Calliope.Tokenizer 3 | import Calliope.Parser 4 | import Calliope.Compiler 5 | 6 | def precompile(haml) do 7 | tokenize(haml) |> parse |> compile 8 | end 9 | 10 | def eval(html, []), do: html 11 | def eval(html, args), do: EEx.eval_string(html, args) 12 | 13 | defmacro __using__([]) do 14 | quote do 15 | import unquote __MODULE__ 16 | import Calliope.Tokenizer 17 | import Calliope.Parser 18 | import Calliope.Compiler 19 | import Calliope.Safe 20 | 21 | require EEx 22 | 23 | def render(haml, args \\ []) do 24 | precompile(haml) |> eval(args) 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/calliope/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule CalliopeException do 2 | defexception [:message] 3 | 4 | def messages do 5 | [ 6 | too_deep_indent: "Indentation was too deep on line number: #", 7 | unknown_filter: "Unknown filter on line number: #", 8 | multiple_ids_assigned: "tag id is assigned multiple times on line number #", 9 | invalid_attribute: "Invalid attribute '##data##' on line number #`", 10 | unknown: "Something wicked this way comes" 11 | ] 12 | end 13 | 14 | def exception(opts) do 15 | error = error_message(opts) 16 | line = line_number(opts) 17 | data = get_data(opts) 18 | 19 | %CalliopeException{message: build_message(error, line, data)} 20 | end 21 | 22 | defp build_message(error, line, data) do 23 | messages[error] 24 | |> String.replace(~r/##data##/, data) 25 | |> String.replace(~r/#/, "#{line}") 26 | end 27 | defp error_message(opts), do: opts[:error] || :unknown 28 | defp line_number(opts), do: opts[:line] || "unknown" 29 | defp get_data(opts), do: opts[:data] || "" 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/calliope/engine_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Simple do 2 | use Calliope.Engine, [ 3 | alias: "simple", 4 | templates: "test/fixtures", 5 | layout: :none 6 | ] 7 | end 8 | 9 | defmodule SimpleLayout do 10 | use Calliope.Engine, [ 11 | alias: nil, 12 | templates: "test/fixtures/simple", 13 | layout: "application" 14 | ] 15 | end 16 | 17 | defmodule CalliopeEngineTest do 18 | use ExUnit.Case 19 | 20 | import Simple 21 | 22 | test :render_pages do 23 | assert "
<%= var %>
}, "") 68 | 69 | haml = """ 70 | - var = "test" 71 | %p= var 72 | """ 73 | assert expected == Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, render(haml), "") 74 | end 75 | 76 | test :case_evaluation do 77 | haml = ~s{- case @var do 78 | - nil -> 79 | %p Found nil value 80 | - other -> 81 | %p Found other: 82 | = other 83 | } 84 | 85 | expected = ~s{<%= case @var do %> 86 | <% nil -> %> 87 |Found nil value
88 | <% other -> %> 89 |Found other: 90 | <%= other %>
91 | <% end %> 92 | } 93 | assert String.replace(expected, "\n", "") == String.replace(render(haml), "\n", "") 94 | end 95 | 96 | test :else_result do 97 | haml = """ 98 | - if false do 99 | %p true 100 | - else 101 | %p false 102 | """ 103 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(haml), []), "") 104 | assert actual == "false
" 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/calliope/engine.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Engine do 2 | 3 | import Calliope.Render 4 | 5 | @doc """ 6 | The Calliope Engine allows you to precompile your haml templates to be accessed 7 | through functions at runtime. 8 | 9 | Example: 10 | 11 | defmodule Simple do 12 | 13 | use Calliope.Engine 14 | 15 | def show do 16 | content_for(:show, [title: Calliope]) 17 | end 18 | 19 | end 20 | 21 | The configure options are: 22 | 23 | `:path` - provides the root path. The default is the current working directory. 24 | `:templates` - used to define where the templates are stored. 25 | `:alias` - used to set the directory where the templates are located. The 26 | default value is 'templates'. 27 | `:layout` - the layout to use for templates. The default is `:none` or you can pass in 28 | the name of a layout. 29 | `:layout_directory` - the directory that your layouts are stored relative to the 30 | templates path. The default directory is `layouts` 31 | 32 | """ 33 | 34 | defmacro __using__(opts \\ []) do 35 | dir = Keyword.get(opts, :alias, "templates") 36 | templates = Keyword.get(opts, :templates, nil) 37 | root = Keyword.get(opts, :path, File.cwd!) 38 | layout = Keyword.get(opts, :layout, :none) 39 | layout_directory = Keyword.get(opts, :layout_directory, "layouts") 40 | 41 | path = build_path_for [root, templates, dir] 42 | layout_path = build_path_for [root, templates, layout_directory] 43 | 44 | quote do 45 | import unquote(__MODULE__) 46 | 47 | use Calliope.Render 48 | 49 | compile_layout unquote(layout), unquote(layout_path) 50 | 51 | compile_templates unquote(path) 52 | 53 | def layout_for(content, args\\[]) do 54 | content_for unquote(layout), [ yield: content ] ++ args 55 | end 56 | 57 | def content_with_layout(name, args) do 58 | content_for(name, args) |> layout_for(args) 59 | end 60 | 61 | def content_for(:none, args) do 62 | Keyword.get(args, :yield, "") |> Calliope.Render.eval(args) 63 | end 64 | end 65 | end 66 | 67 | defmacro compile_layout(:none, _path), do: nil 68 | defmacro compile_layout(_layout, path) do 69 | quote do 70 | compile_templates unquote(path) 71 | end 72 | end 73 | 74 | defmacro compile_templates(path) do 75 | path = eval_path(path) 76 | quote do: unquote files_for(path) |> haml_views |> view_to_function(path) 77 | end 78 | 79 | def build_path_for(list), do: Enum.filter(list, fn(x) -> is_binary x end) |> Enum.join("/") 80 | 81 | def eval_path(path) do 82 | { path, _ } = Code.eval_quoted path 83 | path 84 | end 85 | 86 | def files_for(nil), do: [] 87 | def files_for(path), do: File.ls! path 88 | 89 | def haml_views(files) do 90 | Enum.filter(files, fn(v) -> Regex.match?(~r{^\w*\.html\.haml$}, v) end) 91 | end 92 | 93 | def precompile_view(path), do: File.read!(path) |> precompile 94 | 95 | def view_to_function([], _), do: "" 96 | def view_to_function([view|t], path) do 97 | [ name, _, _ ] = String.split(view, ".") 98 | 99 | content = precompile_view path <> "/" <> view 100 | 101 | quote do 102 | def content_for(unquote(String.to_atom name), args) do 103 | Calliope.Render.eval unquote(content), args 104 | end 105 | def content_for(unquote(name), args) do 106 | Calliope.Render.eval unquote(content), args 107 | end 108 | 109 | unquote(view_to_function(t, path)) 110 | end 111 | end 112 | 113 | end 114 | -------------------------------------------------------------------------------- /test/calliope/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeIntegerationTest do 2 | use ExUnit.Case 3 | 4 | use Calliope.Render 5 | 6 | @haml """ 7 | - if false do 8 | %p true 9 | - else 10 | %p false 11 | """ 12 | test :else_result do 13 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 14 | assert actual == "false
" 15 | end 16 | 17 | @haml """ 18 | - if true do 19 | %p true 20 | - else 21 | %p false 22 | """ 23 | test :if_result_with_else do 24 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 25 | assert actual == "true
" 26 | end 27 | 28 | @haml """ 29 | - if true do 30 | %p true 31 | """ 32 | test :if_true_result do 33 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 34 | assert actual == "true
" 35 | end 36 | 37 | @haml """ 38 | - if false do 39 | %p true 40 | """ 41 | test :if_false_result do 42 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 43 | assert actual == "" 44 | end 45 | 46 | @haml """ 47 | - unless false do 48 | %p true 49 | """ 50 | test :uneless_false_result do 51 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 52 | assert actual == "true
" 53 | end 54 | 55 | @haml ~S(- unless true do 56 | %p false 57 | - else 58 | %p true 59 | ) 60 | test :unless_result_with_else do 61 | actual = EEx.eval_string(render(@haml), []) 62 | assert actual == "\ntrue
\n" 63 | end 64 | 65 | @haml ~S(- answer = "42" 66 | %p 67 | The answer is 68 | = " #{answer}" 69 | ) 70 | test :local_variable do 71 | actual = EEx.eval_string(render(@haml), []) 72 | assert actual == "\n The answer is\n 42\n
\n" 73 | end 74 | 75 | @haml ~S( 76 | - for x <- [1,2] do 77 | %p= x 78 | ) 79 | test :for_evaluation do 80 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 81 | assert actual == "1
2
" 82 | end 83 | 84 | @haml ~S( 85 | - case 1 + 1 do 86 | - 1 -> 87 | %p Got one 88 | - 2 -> 89 | %p Got two 90 | - other -> 91 | %p= "Got other #{other}" 92 | ) 93 | test :case_evaluation do 94 | actual = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, EEx.eval_string(render(@haml), []), "") 95 | assert actual == "Got two
" 96 | end 97 | 98 | @haml ~S( 99 | .simple_div 100 | %b Label: 101 | Content 102 | Outside the div 103 | ) 104 | @expected ~S(No1
145 | <% (2 * 2 != 4) -> %> 146 |No2
147 | <% true -> %> 148 |Yes
149 | <% end %>}, "") 150 | 151 | parsed_tokens = [ 152 | [ indent: 1, smart_script: "cond do", children: [ 153 | [ indent: 2, smart_script: "(1 + 1 == 1) ->", children: [[ indent: 3, tag: "p", content: "No1" ]]], 154 | [ indent: 2, smart_script: "(2 * 2 != 4) ->", children: [[ indent: 3, tag: "p", content: "No2" ]]], 155 | [ indent: 2, smart_script: "true ->", children: [[ indent: 3, tag: "p", content: "Yes" ]]]]]] 156 | 157 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 158 | 159 | assert expected_results == compiled_results 160 | end 161 | 162 | test :compile_with_if_evaluation do 163 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 164 | <%= if test > 5 do %> 165 |No1
166 | <% end %>}, "") 167 | 168 | parsed_tokens = [ 169 | [ indent: 1, smart_script: "if test > 5 do", children: [[ indent: 2, tag: "p", content: "No1" ]]], 170 | ] 171 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 172 | 173 | assert expected_results == compiled_results 174 | end 175 | 176 | test :compile_with_if_else_evaluation do 177 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 178 | <%= if test > 5 do %> 179 |No1
180 | <% else %> 181 |No2
182 | <% end %>}, "") 183 | 184 | parsed_tokens = [ 185 | [ indent: 1, smart_script: "if test > 5 do", children: [[ indent: 2, tag: "p", content: "No1" ]]], 186 | [ indent: 1, smart_script: "else", children: [[indent: 2, tag: "p", content: "No2" ]]] 187 | ] 188 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 189 | 190 | assert expected_results == compiled_results 191 | end 192 | 193 | test :compile_with_unless_evaluation do 194 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 195 | <%= unless test > 5 do %> 196 |No1
197 | <% end %>}, "") 198 | 199 | parsed_tokens = [ 200 | [ indent: 1, smart_script: "unless test > 5 do", children: [[ indent: 2, tag: "p", content: "No1" ]]], 201 | ] 202 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 203 | 204 | assert expected_results == compiled_results 205 | end 206 | 207 | test :compile_with_unless_else_evaluation do 208 | expected_results = ~s{<%= unless test > 5 do %> 209 |No1
210 | <% else %> 211 |No2
212 | <% end %>} 213 | 214 | parsed_tokens = [ 215 | [ indent: 1, smart_script: "unless test > 5 do", children: [[ indent: 2, tag: "p", content: "No1" ]]], 216 | [ indent: 1, smart_script: "else", children: [[indent: 2, tag: "p", content: "No2" ]]] 217 | ] 218 | compiled_results = compile(parsed_tokens) 219 | assert expected_results == compiled_results 220 | end 221 | 222 | test :compile_local_variables do 223 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 224 | <% test = "testing" %> 225 | <%= test %>}, "") 226 | 227 | parsed_tokens = [ 228 | [smart_script: "test = \"testing\"", line_number: 1], 229 | [script: " test", line_number: 2] 230 | ] 231 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 232 | assert expected_results == compiled_results 233 | end 234 | 235 | test :preserves_indentation_and_new_lines do 236 | expected = "