├── 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 "

Calliope

\nIndex Page\n" == content_for :index, [title: "Calliope"] 24 | end 25 | 26 | test :content_for_multiple_views do 27 | assert "This is the edit page\n" == content_for :edit, [] 28 | assert "This is the show page\n" == content_for :show, [] 29 | assert "This is foo\n" == content_for :foo, [] 30 | end 31 | end 32 | 33 | defmodule CalliopeEngineLayoutTest do 34 | use ExUnit.Case 35 | 36 | import SimpleLayout 37 | 38 | test :render_page_with_layout do 39 | content = "

The Application Layout

\n

Calliope

\nIndex Page\n\n" 40 | 41 | assert content == content_with_layout :index, [title: "Calliope"] 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/calliope/safe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeSafeTest do 2 | use ExUnit.Case 3 | 4 | import Calliope.Safe 5 | 6 | test :eval_safe_script do 7 | assert "<script&rt;a bad script</script&rt;" == 8 | eval_safe_script "val", [val: ""] 9 | 10 | assert "" == 11 | eval_safe_script "Safe.script(val)", [val: ""] 12 | assert "" == 13 | eval_safe_script "Safe.script val", [val: ""] 14 | end 15 | 16 | test :clean do 17 | assert "<script&rt;a bad script</script&rt;" == 18 | clean "" 19 | assert [ arg: "<script&rt;a bad script & more</script&rt;" ] == 20 | clean [ arg: "" ] 21 | assert [ posts: [ {1, "<script&rt;a bad script</script&rt;"}, {2, "ok"} ] ] == 22 | clean [ posts: [ {1, ""}, { 2, "ok" } ] ] 23 | assert [ list: [ "<script&rt;a bad script</script&rt;", "ok" ] ] == 24 | clean [ list: [ "", "ok" ] ] 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/simple_example.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html{lang: "en-US"} 3 | %head 4 | %title Welcome to #{title} 5 | %meta{charset: "UTF-8"} 6 | %meta{content: "width=device-width", "maximum-scale": "1.0,", name: "viewport", "user-scalable": "0,"} 7 | /[if IE] 8 | 9 | %link{href: "/static/stylesheets/application.css", media: "screen", rel: "stylesheet", type: "text/css"} 10 | %link{href: "http://fonts.googleapis.com/css?family=Raleway:800,200|Merriweather:400,400italic", rel: "stylesheet", type: "text/css"} 11 | %body 12 | %header 13 | .container 14 | %h1 15 | %a{href: url} Welcome to #{title} 16 | .session 17 | %a.username{href: "#"} Achilles 18 | %a.button.signinout{href: "/authentication/destroy"} Sign Out 19 | .container 20 | %section 21 | %article.blog_post 22 | %h1 23 | %a{href: "#"} Learning about Haml 24 | %p 25 | You can learn more about Haml by taking the time to RTFM 26 | %a.button.readmore{href: "#"} Read More 27 | %article.blog_post 28 | %h1 29 | %a{href: "#"} Learning about #{title} 30 | %p 31 | See the first post 32 | %a.button.readmore{href: "#"} Read More 33 | %footer 34 | .container 35 | %p 36 | A haml parser 37 | %a{href: "http://www.github.com/nurugger07", target: "_blank"} Johnny Winn 38 | -------------------------------------------------------------------------------- /lib/calliope/safe.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Safe do 2 | 3 | @doc """ 4 | Calliope.Safe is a utility for evaluating code and escaping HTML tag 5 | characters. 6 | """ 7 | 8 | @html_escape [ 9 | { "&", "&"}, 10 | { "<", "<" }, 11 | { ">", "&rt;" }, 12 | { "\"", ""e;" }, 13 | { "'", "'" }, 14 | ] 15 | 16 | def eval_safe_script("Safe.script" <> script, args) do 17 | evaluate_script(args, script) 18 | end 19 | def eval_safe_script(script, args) do 20 | clean args |> evaluate_script(script) 21 | end 22 | 23 | def evaluate_script(args, script) do 24 | { result, _ } = Code.string_to_quoted!(script) |> Code.eval_quoted(args) 25 | result 26 | end 27 | 28 | def clean(str) when is_binary(str), do: scrub(str, @html_escape) 29 | def clean([]), do: [] 30 | def clean([{ arg, val} | t ]) when is_binary(val) do 31 | [ { arg, scrub(val, @html_escape) } | clean(t) ] 32 | end 33 | def clean([{ arg, val} | t]) when is_list(val) do 34 | [ { arg, scrub_list(val) } | clean(t) ] 35 | end 36 | 37 | defp scrub_list([]), do: [] 38 | defp scrub_list([h|t]) when is_tuple(h) do 39 | [(Tuple.to_list(h) |> scrub_list |> List.to_tuple)] ++ scrub_list(t) 40 | end 41 | defp scrub_list([h|t]) do 42 | [scrub(h, @html_escape) | scrub_list(t)] 43 | end 44 | 45 | defp scrub(val, []), do: val 46 | defp scrub(val, [{ html, escape } | t]) do 47 | escape_string(val, html, escape) |> scrub(t) 48 | end 49 | 50 | defp escape_string(str, _, _) when is_integer(str), do: str 51 | defp escape_string(str, element, replace) do 52 | String.replace(str, element, replace) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/calliope/tokenizer.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Tokenizer do 2 | 3 | @indent ~S/(^[\t ]+)|(\/\s)|(\/\[\w+])/ 4 | @tag_class_id ~S/([%.#][-:\w]+)/ 5 | @keyword ~S/[-:\w]+/ 6 | @value ~S/(?:(?:'.*?')|(?:".*?"))/ 7 | @hash_param ~s/\\s*#{@keyword}:\\s*#{@value}\\s*/ 8 | @hash_params ~s/({#{@hash_param}(?:,#{@hash_param})*?})/ 9 | @h_r_param ~s/\\s*:[-\\w]+?\\s+?=\\>\\s+?#{@value}\\s*/ 10 | @h_r_params ~s/({#{@h_r_param}(?:,#{@h_r_param})*?})/ 11 | @html_param ~s/\\s*#{@keyword}\\s*=\\s*#{@value}\\s*/ 12 | @html_params ~s/(\\(#{@html_param}(?:\\s#{@html_param})*?\\))/ 13 | @rest ~S/(.+)/ 14 | 15 | @regex ~r/(?:#{@indent}|#{@tag_class_id}|#{@hash_params}|#{@h_r_params}|#{@html_params}|#{@rest})/ 16 | 17 | def tokenize(haml) when is_binary(haml) do 18 | Regex.split(~r/\n/, haml, trim: true) |> tokenize |> tokenize_identation |> index 19 | end 20 | 21 | def tokenize([]), do: [] 22 | def tokenize([h|t]) do 23 | [tokenize_line(h) | tokenize(t)] 24 | end 25 | 26 | def tokenize_line(line) do 27 | Regex.scan(@regex, line, trim: true) |> reduce 28 | end 29 | 30 | def reduce([]), do: [] 31 | def reduce([h|t]) do 32 | [Enum.reverse(h) |> hd | reduce(t)] 33 | end 34 | 35 | def tokenize_identation(list), do: tokenize_identation(list, compute_tabs(list)) 36 | def tokenize_identation([], _), do: [] 37 | def tokenize_identation([h|t], spacing) do 38 | [head|tail] = h 39 | new_head = cond do 40 | Regex.match?(~r/^ +$/, head) -> [replace_with_tabs(head, spacing) | tail] 41 | true -> h 42 | end 43 | 44 | [new_head | tokenize_identation(t, spacing)] 45 | end 46 | 47 | defp replace_with_tabs(empty_str, spacing) do 48 | div(String.length(empty_str), spacing) |> add_tab 49 | end 50 | 51 | def compute_tabs([]), do: 0 52 | def compute_tabs([h|t]) do 53 | [head|_] = h 54 | cond do 55 | Regex.match?(~r/^ +$/, head) -> String.length head 56 | true -> compute_tabs(t) 57 | end 58 | end 59 | 60 | defp add_tab(n), do: add_tab(n, "") 61 | defp add_tab(0, acc), do: acc 62 | defp add_tab(n, acc), do: add_tab(n-1, "\t" <> acc) 63 | 64 | def index(list, i\\1) 65 | def index([], _), do: [] 66 | def index([h|t], i), do: [[i | h] | index(t, i+1)] 67 | 68 | end 69 | -------------------------------------------------------------------------------- /test/calliope/render_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeRenderTest do 2 | use ExUnit.Case 3 | 4 | use Calliope.Render 5 | 6 | @haml ~s{!!! 5 7 | %section.container(class= "blue" style="margin-top: 10px" data-value=@value) 8 | %article 9 | %h1= title 10 | :javascript 11 | var x = 5; 12 | var y = 10; 13 | / %h1 An important inline comment 14 | /[if IE] 15 | %h2 An Elixir Haml Parser 16 | %label.cl1\{ for: "test", class: " cl2" \} Label 17 | #main.content 18 | Welcome to Calliope} 19 | 20 | @html ~s{ 21 |
22 |
23 |

<%= title %>

24 | 28 | 29 | 30 | 31 |
32 | Welcome to Calliope 33 |
34 |
35 |
36 | } 37 | 38 | @haml_with_args "%a{href: '#\{url}'}= title" 39 | 40 | test :render do 41 | assert String.replace(@html, "\n", "") == String.replace(render(@haml), "\n", "") 42 | assert "

This is <%= title %>

\n" == render "%h1 This is \#{title}" 43 | assert "Click Me\n" == render "%a{ng-click: 'doSomething()'} Click Me" 44 | assert "

{{user}}

\n" == render "%h1 {{user}}" 45 | end 46 | 47 | test :eval do 48 | result = "Example\n" 49 | assert result == render "%a{href: 'http://example.com'} Example" |> eval([]) 50 | assert result == render(~s(%a{href: 'http://example.com'}= "Example")) |> eval([conn: []]) 51 | end 52 | 53 | test :render_with_params do 54 | assert "<%= title %>\n" == 55 | render @haml_with_args 56 | end 57 | 58 | test :render_with_args do 59 | assert "Google\n" == 60 | render @haml_with_args, [ url: "http://google.com", title: "Google" ] 61 | end 62 | 63 | test :local_variable do 64 | 65 | expected = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 66 | <% var = "test" %> 67 |

<%= 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 == "\n

true

\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(
105 | Label: 106 | Content 107 |
108 | Outside the div 109 | ) 110 | test :preserves_newlines do 111 | assert EEx.eval_string(render(@haml), []) == @expected 112 | end 113 | 114 | 115 | @haml ~s{!!! 5 116 | %section.container 117 | %h1 118 | = arg 119 | 120 | 121 | #main.content 122 | Welcome to Calliope 123 | %br 124 | %section.container 125 | %img(src='#') 126 | } 127 | 128 | @expected ~s{ 129 |
130 |

131 | <%= arg %> 132 |

133 | 134 | 135 |
136 | Welcome to Calliope 137 |
138 |
139 |
140 |
141 | 142 |
143 | } 144 | test :preserves_newlines_with_comments do 145 | assert render(@haml) == @expected 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/calliope/tokenizer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeTokenizerTest do 2 | use ExUnit.Case 3 | 4 | import Calliope.Tokenizer 5 | 6 | @haml ~s{ 7 | !!! 5 8 | %section.container 9 | %h1 Calliope 10 | / %h1 An important inline comment 11 | /[if IE] 12 | %h2 An Elixir Haml Parser 13 | .content 14 | = arg 15 | Welcome to Calliope} 16 | 17 | @haml_with_collection """ 18 | - for { content } <- posts do 19 | %div 20 | = content 21 | """ 22 | 23 | @haml_with_haml_comments """ 24 | %p foo 25 | -# This would 26 | Not be 27 | output 28 | %p bar 29 | """ 30 | 31 | test :tokenize_inline_haml do 32 | inline = "%div Hello Calliope" 33 | assert [[1, "%div"," Hello Calliope"]] == tokenize(inline) 34 | assert [[1, "%h1", " This is \#{title}"]] == tokenize("%h1 This is \#{title}") 35 | 36 | inline = "%a{ng-click: 'doSomething()'}Click Me" 37 | assert [[1, "%a", "{ng-click: 'doSomething()'}", "Click Me"]] == tokenize inline 38 | 39 | inline = "%h1 {{user}}" 40 | assert [[1, "%h1", " {{user}}"]] == tokenize inline 41 | end 42 | 43 | test :tokenize_multiline_haml do 44 | assert [ 45 | [1, "!!! 5"], 46 | [2, "%section", ".container"], 47 | [3, "\t", "%h1", " Calliope"], 48 | [4, "\t", "/ ", "%h1", " An important inline comment"], 49 | [5, "\t", "/[if IE]"], 50 | [6, "\t\t", "%h2", " An Elixir Haml Parser"], 51 | [7, "\t", ".content"], 52 | [8, "\t\t", "= arg"], 53 | [9, "\t\t", "Welcome to Calliope"] 54 | ] == tokenize(@haml) 55 | 56 | assert [ 57 | [1, "- for { content } <- posts do"], 58 | [2, "\t", "%div"], 59 | [3, "\t\t", "= content"] 60 | ] == tokenize(@haml_with_collection) 61 | 62 | assert [ 63 | [1, "%p", " foo"], 64 | [2, "\t", "-# This would"], 65 | [3, "\t\t", "Not be"], 66 | [4, "\t\t", "output"], 67 | [5, "%p", " bar"] 68 | ] == tokenize(@haml_with_haml_comments) 69 | end 70 | 71 | test :tokenize_line do 72 | assert [[1, "%section", ".container", ".blue", "{src:'#', data:'cool'}", " Calliope"]] == 73 | tokenize("\n%section.container.blue{src:'#', data:'cool'} Calliope") 74 | assert [[1, "%section", ".container", "(src='#' data='cool')", " Calliope"]] == 75 | tokenize("\n%section.container(src='#' data='cool') Calliope") 76 | assert [[1, "\t", "%a", "{href: \"#\"}", " Learning about \#{title}"]] == 77 | tokenize("\t%a{href: \"#\"} Learning about \#{title}") 78 | 79 | # allowing spaces after the attribute values before closing curly brace 80 | assert [[1, "%label", ".cl1", "{ for: 'test', class: ' cl2' }", " Label" ]] == 81 | tokenize("%label.cl1{ for: 'test', class: ' cl2' } Label") 82 | 83 | assert [[1, "%label", "{ for: \"\#{@id}\", class: \"\#{@class}\" }", " Label" ]] == 84 | tokenize("%label{ for: \"\#{@id}\", class: \"\#{@class}\" } Label") 85 | end 86 | 87 | test :tokenize_identation do 88 | assert [ 89 | ["%section"], 90 | ["\t", "%h1", "Calliope"], 91 | ["\t", "%h2", "Subtitle"], 92 | ["\t\t", "%section"] 93 | ] == tokenize_identation [ 94 | ["%section"], 95 | [" ", "%h1", "Calliope"], 96 | [" ", "%h2", "Subtitle"], 97 | [" ", "%section"] 98 | ], 2 99 | end 100 | 101 | test :index do 102 | assert [ 103 | [1, "%section"], 104 | [2, "\t", "%h1", "Calliope"], 105 | [3, "\t", "%h2", "Subtitle"], 106 | [4, "\t\t", "%section"] 107 | ] == index [ 108 | ["%section"], 109 | ["\t", "%h1", "Calliope"], 110 | ["\t", "%h2", "Subtitle"], 111 | ["\t\t", "%section"] 112 | ] 113 | end 114 | 115 | test :compute_tabs do 116 | assert 0 == compute_tabs [["aa"]] 117 | assert 2 == compute_tabs [["aa"], [" ", "aa"]] 118 | assert 4 == compute_tabs [["aa"], [" ", "aa"]] 119 | assert 2 == compute_tabs [["aa"], [" ", "aa"], [" ", "aa"]] 120 | end 121 | 122 | test :parse_with_space do 123 | assert [[1, "%h1", " (foo)"]] == tokenize("%h1 (foo)") 124 | assert [[1, "%h1", "(foo)"]] == tokenize("%h1(foo)") 125 | end 126 | 127 | test :parse_with_content do 128 | assert [[1, "%h1", " foo"]] == tokenize("%h1 foo") 129 | end 130 | 131 | test :hash_rocket do 132 | result = tokenize ~S[%p.alert.alert-info{:style => "one"}= get_flash(@conn, :info)] 133 | expected = [[1, "%p", ".alert", ".alert-info", "{:style => \"one\"}", "= get_flash(@conn, :info)"]] 134 | assert expected == result 135 | end 136 | 137 | end 138 | -------------------------------------------------------------------------------- /lib/calliope/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Parser do 2 | 3 | @tag "%" 4 | @id "#" 5 | @class "." 6 | @content " " 7 | @tab "\t" 8 | @doctype "!" 9 | @attrs "{" 10 | @parens "(" 11 | @comment "/" 12 | @script "=" 13 | @smart "-" 14 | @filter ":" 15 | 16 | def parse([]), do: [] 17 | def parse(l) do 18 | parse_lines(l) |> validations |> build_tree 19 | end 20 | 21 | def parse_lines([]), do: [] 22 | def parse_lines([h|t]), do: [parse_line(h)|parse_lines(t)] 23 | 24 | def parse_line(list, acc \\ []) 25 | def parse_line([], acc), do: acc 26 | def parse_line([h|t], acc) when is_integer(h) do 27 | parse_line(t, [line_number: h] ++ acc) 28 | end 29 | def parse_line([h|t], acc) do 30 | [sym, val] = [head(h), tail(h)] 31 | acc = case sym do 32 | @doctype -> [ doctype: h ] ++ acc 33 | @tag -> [ tag: String.strip(val) ] ++ acc 34 | @id -> handle_id(acc, val) 35 | @class -> merge_into(:classes, acc, [val]) 36 | @tab -> [ indent: String.length(h) ] ++ acc 37 | @attrs -> merge_attributes( acc, val) 38 | @parens -> merge_attributes( acc, val) 39 | @comment -> handle_comment(val) ++ acc 40 | @script -> [ script: val ] ++ acc 41 | @smart -> [ smart_script: String.strip(val) ] ++ acc 42 | @filter -> handle_filter(acc, val) 43 | _ -> [ content: String.strip(h) ] ++ acc 44 | end 45 | parse_line(t, acc) 46 | end 47 | 48 | def handle_comment(val), do: [ comment: String.rstrip "!--#{val}" ] 49 | def handle_id(line, id) do 50 | cond do 51 | line[:id] -> raise_error :multiple_ids_assigned, line[:line_number] 52 | true -> [ id: id ] ++ line 53 | end 54 | end 55 | 56 | def handle_filter(line, "javascript") do 57 | [ tag: "script", attributes: "type=\"text/javascript\"" ] ++ line 58 | end 59 | def handle_filter(line, _unknown_filter) do 60 | raise_error(:unknown_filter, line[:line_number]) 61 | end 62 | 63 | def build_attributes(value) do 64 | String.slice(value, 0, String.length(value)-1) |> 65 | String.replace(~r/(? 66 | String.replace(~r/(? 67 | String.replace(~r/:\s+([\'"])/, "=\\1") |> 68 | String.replace(~r/[:=]\s?(?!.*["'])(@?\w+)\s?/, "='#\{\\1}'") |> 69 | String.replace(~r/[})]$/, "") |> 70 | String.replace(~r/"(.+?)"\s=>\s(@?\w+)\s?/, "\\1='#\{\\2}'") |> 71 | String.replace(~r/:(.+?)\s=>\s['"](.*)['"]\s?/, "\\1='\\2'") |> 72 | filter_commas |> 73 | String.strip 74 | end 75 | 76 | @empty_param ~S/^\s*?[-\w]+?\s*?$/ 77 | @empty_params ~s/(#{@empty_param})+?/ 78 | @param1 ~S/[-\w]+?\s*?=\s*?['"].*?['"]\s*?/ 79 | @params ~s/(#{@param1})+?/ 80 | @validate ~s/^(#{@params})|(#{@empty_params})$/ 81 | 82 | def validate_attributes(attributes) do 83 | if Regex.match?(~r/#{@validate}/, attributes) || attributes == "" do 84 | {:ok, attributes} 85 | else 86 | {:error, attributes} 87 | end 88 | end 89 | 90 | @wraps [?', ?"] 91 | 92 | def filter_commas(string) do 93 | state = String.to_char_list(string) 94 | |> Enum.reduce(%{buffer: [], closing: false}, fn(ch, state) -> 95 | {char, closing} = case {ch, state[:closing]} do 96 | {ch, false} when ch in @wraps -> {ch, ch} 97 | {ch, closing} when ch == closing -> {ch, false} 98 | {?,, false} -> {0, false} 99 | {?,,closing} -> {?,, closing} 100 | {ch, closing} -> {ch, closing} 101 | end 102 | buffer = unless char == 0, do: [char | state[:buffer]], else: state[:buffer] 103 | %{buffer: buffer, closing: closing} 104 | end) 105 | Enum.reverse(state[:buffer]) 106 | |> List.to_string 107 | end 108 | 109 | def build_tree([]), do: [] 110 | def build_tree([h|t]) do 111 | {rem, children} = pop_children(h, t) 112 | [h ++ build_children(children)] ++ build_tree(rem) 113 | end 114 | 115 | defp build_children([]), do: [] 116 | defp build_children(l), do: [children: build_tree(l)] 117 | 118 | defp pop_children(parent, list) do 119 | { children, rem } = Enum.split_while(list, &bigger_indentation?(&1, parent)) 120 | { rem, children } 121 | end 122 | 123 | defp bigger_indentation?(token1, token2) do 124 | Keyword.get(token1, :indent, 0) > Keyword.get(token2, :indent, 0) 125 | end 126 | 127 | defp merge_attributes(list, "{" <> value) do 128 | [content: "{{#{value}"] ++ list 129 | end 130 | defp merge_attributes(list, value) do 131 | classes = extract(:class, value) 132 | id = extract(:id, value) 133 | attributes = case build_attributes(value) |> validate_attributes do 134 | {:ok, attrs} -> 135 | attrs 136 | {:error, attrs} -> 137 | raise_error :invalid_attribute, list[:line_number], attrs 138 | end 139 | 140 | [attributes: attributes] ++ merge_into(:classes, merge_into(:id, list, id), classes) 141 | end 142 | 143 | defp extract(_, nil), do: [] 144 | defp extract(key, str) do 145 | case Regex.run(~r/(? String.split match 147 | _ -> [] 148 | end 149 | end 150 | 151 | defp merge_into(:id, list, []), do: list 152 | defp merge_into(:id, list, [h|_]), do: [ id: h ] ++ list 153 | 154 | defp merge_into(_, list, []), do: list 155 | defp merge_into(key, list, value) do 156 | value = cond do 157 | list[key] -> list[key] ++ value 158 | true -> value 159 | end 160 | Keyword.put(list, key, value) |> Enum.reverse 161 | end 162 | 163 | defp head(str), do: String.first(str) 164 | defp tail(str), do: String.slice(str, 1..-1) 165 | 166 | defp validations([]), do: [] 167 | defp validations([h|t]) do 168 | next = List.first(t) 169 | cond do 170 | invalid_indentation?(h, next) -> raise_error(:too_deep_indent, next[:line_number]) 171 | true -> [h | validations(t)] 172 | end 173 | end 174 | 175 | defp invalid_indentation?(_, nil), do: false 176 | defp invalid_indentation?(parent, child) do 177 | Keyword.get(child, :indent, 0) > Keyword.get(parent, :indent, 0) + 1 178 | end 179 | 180 | defp raise_error(error, line, data \\ nil), do: raise(CalliopeException, error: error, line: line, data: data) 181 | end 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Calliope](http://f.cl.ly/items/0T3a1a1w472z2o3p0d3O/6660441229_f6503a0dd2_b.jpg) 2 | 3 | # Calliope - An Elixir Haml Parser [![Build Status](https://travis-ci.org/nurugger07/calliope.png?branch=master)](https://travis-ci.org/nurugger07/calliope) 4 | 5 | For those of you that prefer the poetic beauty of [HAML](https://github.com/haml/haml) templates over HTML, then Calliope is the package for you. Calliope is a parser written in [Elixir](http://elixir-lang.org/) that will render HAML/Elixir templates into HTML. For example, we can render the following HAML: 6 | 7 | ``` haml 8 | !!! 5 9 | %html{lang: "en-US"} 10 | %head 11 | %title Welcome to Calliope 12 | %body 13 | %h1 Calliope 14 | %h2 The muse of epic poetry 15 | ``` 16 | 17 | Into this HTML: 18 | 19 | ``` html 20 | 21 | 22 | 23 | Welcome to Calliope 24 | 25 | 26 |

Calliope

27 |

The muse of epic poetry

28 | 29 | 30 | ``` 31 | 32 | ## Using 33 | 34 | 35 | Calliope is simple to add to any project. If you are using the hex package manager, just add the following to your mix file: 36 | 37 | ``` elixir 38 | def deps do 39 | [ { :calliope, '~> 0.3.0' } ] 40 | end 41 | ``` 42 | 43 | If you aren't using hex, add the a reference to the github repo. 44 | 45 | ``` elixir 46 | def deps do 47 | [ { :calliope, github: "nurugger07/calliope" } ] 48 | end 49 | ``` 50 | 51 | Then run `mix deps.get` in the shell to fetch and compile the dependencies. Then you can either call to Calliope directly: 52 | 53 | ``` shell 54 | iex(1)> Calliope.render "%h1 Welcome to Calliope" 55 | "

Welcome to Calliope

" 56 | ``` 57 | 58 | Or you can `use` Calliope in a module and call through your module: 59 | 60 | ``` elixir 61 | defmodule MyModule do 62 | use Calliope 63 | end 64 | ``` 65 | 66 | ``` shell 67 | iex(1)> MyModule.render "%h1 Welcome to Calliope" 68 | "

Welcome to Calliope

" 69 | ``` 70 | 71 | ## Formating 72 | 73 | If you are not familiar with HAML syntax I would suggest you checkout the [reference](http://haml.info/docs/yardoc/file.REFERENCE.html) page. Most of the syntax has been accounted for but we are in the process of adding more functionality. 74 | 75 | HAML is basically a whitespace sensitive shorthand for HTML that does not use end-tags. Although Calliope uses HAML formating, it does use its own flavor. Sounds great but what does it look like: 76 | 77 | ``` haml 78 | %tag{ attr: "", attr: "" } Content 79 | ``` 80 | 81 | Or you could use the following: 82 | 83 | ``` haml 84 | %tag(attr="" attr="" ) Content 85 | ``` 86 | 87 | The `id` and `class` attributes can also be assigned directly to the tag: 88 | 89 | ``` haml 90 | %tag#id.class Content 91 | ``` 92 | 93 | If you are creating a div you don't need to include the tag at all. This HAML 94 | 95 | ``` haml 96 | #main 97 | .blue Content 98 | ``` 99 | 100 | Will generate the following HTML 101 | 102 | ``` html 103 |
104 |
105 | Content 106 |
107 |
108 | ``` 109 | 110 | ## Passing Arguments 111 | 112 | The render function will also take a list of named arguments that can be evaluated when compiling the HTML 113 | 114 | Given the following HAML: 115 | 116 | ``` haml 117 | #main 118 | .blue= content 119 | ``` 120 | 121 | Then call render and pass in the `haml` and `content`: 122 | 123 | ``` elixir 124 | Calliope.render haml, [content: "Hello, World"] 125 | ``` 126 | 127 | Calliope will render: 128 | 129 | ``` html 130 |
131 |
132 | Hello, World 133 |
134 |
135 | ``` 136 | 137 | ## Embedded Elixir 138 | 139 | Calliope doesn't just evaluate arguments, you can actually embed Elixir directly into the templates: 140 | 141 | ### for 142 | 143 | ``` haml 144 | - for { id, headline, content } <- posts do 145 | %h1 146 | %a{href: "posts/#{id}"}= headline 147 | .content 148 | = content 149 | ``` 150 | 151 | Pass that to `render` with a list of posts 152 | 153 | ``` elixir 154 | Calliope.render haml, [posts: [{1, "Headline 1", "Content 1"}, {2, "Headline 2", "Content 2"}] 155 | ``` 156 | 157 | Will render 158 | 159 | ``` html 160 |

161 | Headline 1 162 |

163 |
164 | Content 1 165 |
166 |

167 | Headline 2 168 |

169 |
170 | Content 2 171 |
172 | ``` 173 | 174 | ### if, else, and unless 175 | 176 | ``` haml 177 | - if post do 178 | %h1= post.title 179 | - if post.comments do 180 | %p Has some comments 181 | - else 182 | %p No Comments 183 | - unless user_guest(user) 184 | %a{href: "posts/edit/#{id}"}= Edit 185 | ``` 186 | 187 | ### case 188 | 189 | ``` haml 190 | - case example do 191 | - "one" -> 192 | %p Example one 193 | - other -> 194 | %p Other Example 195 | #{other} 196 | ``` 197 | 198 | ### Local Variables 199 | 200 | ``` haml 201 | - answer = 42 202 | %p= "What is the answer #{answer}" 203 | ``` 204 | 205 | ### Anonymous Functions 206 | 207 | ``` haml 208 | - form_for @changeset, @action, fn f -> 209 | .form-group 210 | = label f, :name, "Name", class: "control-label" 211 | = text_input f, :name, class: "form-control" 212 | .form-group 213 | = submit "Submit", class: "btn btn-primary" 214 | ``` 215 | 216 | ## Precompile Templates 217 | 218 | Calliope provides an Engine to precompile your haml templates in to functions. This parses the template at compile time and creates a function that takes the name and args needed to render the page. These functions are scoped to the module that uses the engine. 219 | 220 | Adding this functionality is easy. 221 | 222 | ``` elixir 223 | defmodule Simple do 224 | 225 | use Calliope.Engine 226 | 227 | def show do 228 | content_for(:show, [title: Calliope]) 229 | end 230 | 231 | end 232 | ``` 233 | 234 | If you are using layouts, you can set the layout and call the `content_with_layout` function. 235 | 236 | ``` elixir 237 | defmodule Simple do 238 | 239 | use Calliope.Engine, layout: "application" 240 | 241 | def show do 242 | content_with_layout(:show, [title: Calliope]) 243 | end 244 | 245 | end 246 | ``` 247 | 248 | In addition to `:layout`, you can also set the following options: 249 | 250 | `:path` - provides the root path. The default is the current working directory. 251 | `:templates` - used to define where the templates are stored. By default it will use `:path` 252 | `:alias` - used to set the directory where the templates are located. The 253 | default value is 'templates'. 254 | `:layout_directory` - the directory that your layouts are stored relative to the 255 | templates path. The default directory is `layouts` 256 | `:layout` - the layout to use for templates. The default is `:none` or you can pass in 257 | the name of a layout. 258 | 259 | ## Coming Soon 260 | 261 | * Rendering partials 262 | * Exception messages 263 | -------------------------------------------------------------------------------- /lib/calliope/compile.ex: -------------------------------------------------------------------------------- 1 | defmodule Calliope.Compiler do 2 | 3 | @nl "\n" 4 | @indent_size 2 5 | @attributes [ :id, :classes, :attributes ] 6 | @self_closing [ "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr" ] 7 | 8 | @doctypes [ 9 | { :"!!!", ""}, 10 | { :"!!! 5", "" }, 11 | { :"!!! Strict", "" }, 12 | { :"!!! Frameset", "" }, 13 | { :"!!! 1.1", "" }, 14 | { :"!!! Basic", "" }, 15 | { :"!!! Mobile", "" }, 16 | { :"!!! RDFa", "" } 17 | ] 18 | 19 | def compile(list, smart_indent \\ 0) 20 | def compile([], _si), do: "" 21 | def compile(nil, _si), do: "" 22 | def compile([h|t], si) do 23 | build_html(h,t, si) <> compile(t, si) 24 | end 25 | 26 | defp build_html(node, [], si) do 27 | build_html(node, [[]], si) 28 | end 29 | defp build_html(node, [next_node|_], si) do 30 | cond do 31 | node[:smart_script] -> 32 | cond do 33 | next_node[:smart_script] -> 34 | # send next code to look ahead for things like else 35 | evaluate_smart_script(node[:smart_script], node[:children], next_node[:smart_script], si) 36 | true -> 37 | evaluate_smart_script(node[:smart_script], node[:children], "", si) 38 | end 39 | true -> evaluate(node, si) 40 | end 41 | end 42 | 43 | def evaluate(line, si) do 44 | leader(line, si) <> 45 | comment(line[:comment], :open) <> 46 | open(compile_attributes(line), tag(line)) <> 47 | add_newline(line) <> 48 | precompile_content("#{line[:content]}") <> 49 | evaluate_script(line[:script]) <> 50 | compile(line[:children], si) <> 51 | close(tag(line), leader_close(line, si)) <> 52 | comment(line[:comment], :close) <> @nl 53 | end 54 | 55 | defp add_newline(line) do 56 | case line[:children] do 57 | nil -> "" 58 | [] -> "" 59 | _ -> @nl 60 | end 61 | end 62 | 63 | defp leader(line, si) do 64 | case line[:indent] do 65 | nil -> "" 66 | value -> String.rjust("", (value - si) * @indent_size) 67 | end 68 | end 69 | 70 | defp leader_close(line, si) do 71 | case line[:children] do 72 | nil -> "" 73 | [] -> "" 74 | _ -> leader(line, si) 75 | end 76 | end 77 | 78 | def evaluate_smart_script("#" <> _, _, _, _), do: "" 79 | def evaluate_smart_script(script, children, next_node, si) do 80 | smart_script_to_string(script, children, next_node, si) 81 | end 82 | 83 | def evaluate_script(nil), do: "" 84 | def evaluate_script(script) when is_binary(script), do: "<%= #{String.lstrip(script)} %>" 85 | 86 | defp smart_script_to_string("if" <> script, children, "else", si) do 87 | %{cmd: cmd, end_tag: end_tag} = handle_script("if" <> script) 88 | "<%= #{cmd} #{end_tag}" <> smart_children(children, si + 1) 89 | end 90 | defp smart_script_to_string("unless" <> script, children, "else", si) do 91 | %{cmd: cmd, end_tag: end_tag} = handle_script("unless" <> script) 92 | "<%= #{cmd} #{end_tag}" <> smart_children(children, si + 1) 93 | end 94 | defp smart_script_to_string("else", children, _, si) do 95 | %{cmd: cmd, end_tag: end_tag} = handle_script("else") 96 | "<% #{cmd} #{end_tag}" <> smart_children(children, si + 1) <> "<% end %>" 97 | end 98 | 99 | defp smart_script_to_string(script, children, _, si) do 100 | %{cmd: cmd, wraps_end: wraps_end, open_tag: open_tag, 101 | end_tag: end_tag} = handle_script(script) 102 | "#{open_tag} #{cmd} #{end_tag}" <> smart_children(children, si + 1) <> smart_wraps_end(wraps_end) 103 | end 104 | 105 | defp smart_children(nil, _), do: "" 106 | defp smart_children([], _), do: "" 107 | defp smart_children(children, si), do: "\n#{compile children, si}" 108 | 109 | defp smart_wraps_end(""), do: "" 110 | defp smart_wraps_end(nil), do: "" 111 | defp smart_wraps_end(wraps_end), do: "\n#{wraps_end}" 112 | 113 | def precompile_content(nil), do: nil 114 | def precompile_content(content) do 115 | Regex.scan(~r/\#{(.+)}/r, content) |> 116 | map_content_to_args(content) 117 | end 118 | 119 | defp map_content_to_args([], content), do: content 120 | defp map_content_to_args([[key, val]|t], content) do 121 | map_content_to_args(t, String.replace(content, key, "<%= #{val} %>")) 122 | end 123 | 124 | def comment(nil, _), do: "" 125 | def comment("!--", :open), do: "" 127 | 128 | def comment("!--" <> condition, :open), do: "" 130 | 131 | def open( _, nil), do: "" 132 | def open( _, "!!!" <> key), do: @doctypes[:"!!!#{key}"] 133 | def open(attributes, tag_value), do: "<#{tag_value}#{attributes}>" 134 | 135 | def close(tag, leader \\ "") 136 | def close(nil, _leader), do: "" 137 | def close("!!!" <> _, _leader), do: "" 138 | def close(tag_value, _leader) when tag_value in @self_closing, do: "" 139 | def close(tag_value, leader), do: leader <> "" 140 | 141 | def compile_attributes(list) do 142 | Enum.map_join(@attributes, &reject_or_compile_key(&1, list[&1])) |> 143 | precompile_content |> 144 | String.rstrip 145 | end 146 | 147 | def reject_or_compile_key( _, nil), do: nil 148 | def reject_or_compile_key(key, value), do: compile_key({ key, value }) 149 | 150 | def compile_key({ :attributes, value }), do: " #{value}" 151 | def compile_key({ :classes, value}), do: " class=\"#{Enum.join(value, " ")}\"" 152 | 153 | def compile_key({ :id, value }), do: " id=\"#{value}\"" 154 | 155 | def tag(node) do 156 | cond do 157 | has_any_key?(node, [:doctype]) -> Keyword.get(node, :doctype) 158 | has_any_key?(node, [:tag]) -> Keyword.get(node, :tag) 159 | has_any_key?(node, [:id, :classes]) -> "div" 160 | has_any_key?(node, [:content]) -> nil 161 | true -> nil 162 | end 163 | end 164 | 165 | defp has_any_key?( _, []), do: false 166 | defp has_any_key?(list, [h|t]), do: Keyword.has_key?(list, h) || has_any_key?(list, t) 167 | 168 | defp handle_script(script) do 169 | ch = if Regex.match?(~r/^[A-Za-z0-9\?!_\.]+\(/, script), do: ")", else: "" 170 | cond do 171 | Regex.match? ~r/(fn )|(fn\()[a-zA-Z0-9,\) ]+->/, script -> 172 | %{cmd: script, wraps_end: "<% end#{ch} %>", end_tag: "%>", open_tag: "<%="} 173 | Regex.match? ~r/ do:?/, script -> 174 | %{cmd: script, wraps_end: "<% end#{ch} %>", end_tag: "%>", open_tag: "<%="} 175 | true -> 176 | %{cmd: script, wraps_end: "", end_tag: "%>", open_tag: "<%"} 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /test/calliope/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeParserTest do 2 | use ExUnit.Case 3 | 4 | import Calliope.Parser 5 | 6 | @tokens [ 7 | [1, "!!! 5"], 8 | [2, "%section", ".container", ".blue"], 9 | [3, "\t", "%h1", "Calliope"], 10 | [4, "\t", "/", "%h1", "An important inline comment"], 11 | [5, "\t", "/[if IE]"], 12 | [6, "\t\t", "%h2", "An Elixir Haml Parser"], 13 | [7, "\t", "#main", ".content"], 14 | [8, "\t\t", "- for { arg } <- args do"], 15 | [9, "\t\t\t", "= arg"], 16 | [10, "\t\t", " Welcome to \#{title}"], 17 | [11, "%section", ".container", "(data-a: 'calliope', data-b: 'awesome')"], 18 | [12, "\t", "%img", ".one", "{id: 'main_image', class: 'two three', src: url}"], 19 | [13, "\t", ":javascript"] 20 | ] 21 | 22 | @parsed_tokens [ 23 | [ doctype: "!!! 5", line_number: 1], 24 | [ tag: "section", classes: ["container", "blue"] , line_number: 2], 25 | [ indent: 1, tag: "h1", content: "Calliope", line_number: 3 ], 26 | [ indent: 1, comment: "!--", tag: "h1", content: "An important inline comment", line_number: 4 ], 27 | [ indent: 1, comment: "!--[if IE]", line_number: 5 ], 28 | [ indent: 2, tag: "h2", content: "An Elixir Haml Parser", line_number: 6 ], 29 | [ indent: 1, id: "main", classes: ["content"], line_number: 7 ], 30 | [ indent: 2, smart_script: "for { arg } <- args do", line_number: 8 ], 31 | [ indent: 3, script: " arg", line_number: 9 ], 32 | [ indent: 2, content: "Welcome to \#{title}", line_number: 10 ], 33 | [ tag: "section", classes: ["container"], attributes: "data-a='calliope' data-b='awesome'", line_number: 11 ], 34 | [ indent: 1, tag: "img", id: "main_image", classes: ["one", "two", "three"], attributes: "src='\#{url}'", line_number: 12 ], 35 | [ indent: 1, tag: "script", attributes: "type=\"text/javascript\"", line_number: 13 ] 36 | ] 37 | 38 | @nested_tree [ 39 | [ doctype: "!!! 5", line_number: 1], 40 | [ tag: "section", classes: ["container", "blue"], line_number: 2, children: [ 41 | [ indent: 1, tag: "h1", content: "Calliope", line_number: 3 ], 42 | [ indent: 1, comment: "!--", tag: "h1", content: "An important inline comment", line_number: 4 ], 43 | [ indent: 1, comment: "!--[if IE]", line_number: 5, children: [ 44 | [ indent: 2, tag: "h2",content: "An Elixir Haml Parser", line_number: 6] 45 | ] 46 | ], 47 | [ indent: 1, id: "main", classes: ["content"], line_number: 7, children: [ 48 | [ indent: 2, smart_script: "for { arg } <- args do", line_number: 8, children: [ 49 | [ indent: 3, script: " arg", line_number: 9 ] 50 | ] 51 | ], 52 | [ indent: 2, content: "Welcome to \#{title}", line_number: 10 ] 53 | ] 54 | ], 55 | ], 56 | ], 57 | [ tag: "section", classes: ["container"], attributes: "data-a='calliope' data-b='awesome'", line_number: 11, children: [ 58 | [ indent: 1, tag: "img", id: "main_image", classes: ["one", "two", "three"], attributes: "src='\#{url}'", line_number: 12 ], 59 | [ indent: 1, tag: "script", attributes: "type=\"text/javascript\"", line_number: 13 ] 60 | ] 61 | ] 62 | ] 63 | 64 | @tokens_with_haml_comment [ 65 | [1, "%p", "foo"], 66 | [2, "\t", "-# This would"], 67 | [3, "\t\t", "Not be"], 68 | [4, "\t\t", "output"], 69 | [5, "%p", "bar"] 70 | ] 71 | 72 | @parsed_with_haml_comment [ 73 | [ content: "foo", tag: "p", line_number: 1,children: [ 74 | [ smart_script: "# This would", indent: 1, line_number: 2, children: [ 75 | [ content: "Not be", indent: 2, line_number: 3], 76 | [ content: "output", indent: 2, line_number: 4] 77 | ], 78 | ] 79 | ] 80 | ], 81 | [ content: "bar", tag: "p", line_number: 5 ] 82 | ] 83 | 84 | test :parse do 85 | assert @parsed_with_haml_comment == parse @tokens_with_haml_comment 86 | end 87 | 88 | test :parse_with_special_cases do 89 | handle_bars = [[1, "%h1", "{{user}}"]] 90 | parsed_handle_bars = [[ content: "{{user}}", tag: "h1", line_number: 1 ]] 91 | assert parsed_handle_bars == parse handle_bars 92 | end 93 | 94 | test :parse_with_no_space_after_tag do 95 | assert [[attributes: "foo", tag: "h1", line_number: 1]] == parse([[1, "%h1", "(foo)"]]) 96 | end 97 | 98 | test :parse_with_space_after_tag do 99 | assert [[content: "(foo)", tag: "h1", line_number: 1]] == parse([[1, "%h1 ", " (foo)"]]) 100 | end 101 | 102 | test :parse_line do 103 | each_token_with_index fn({ token, index }) -> 104 | assert parsed_tokens(index) == parsed_line_tokens(token) 105 | end 106 | end 107 | 108 | test :build_tree do 109 | assert @nested_tree == build_tree @parsed_tokens 110 | end 111 | 112 | test :build_attributes do 113 | assert "class='#\{@class_name}'" == build_attributes("class: @class_name }") 114 | assert "for='name'" == build_attributes("for: 'name' }") 115 | assert "class='#\{@class_name}'" == build_attributes("class=@class_name }") 116 | assert "style='margin-top: 5px'" == build_attributes("style: 'margin-top: 5px' }") 117 | assert "style=\"margin-top: 5px\"" == build_attributes("style: \"margin-top: 5px\" }") 118 | assert "href='http://google.com'" == build_attributes("href: 'http://google.com' }") 119 | assert "src='#\{url}'" == build_attributes("src: url }") 120 | assert "some-long-value='#\{@value}'" == build_attributes("\"some-long-value\" => @value }") 121 | assert "href=\"#\{fun(one, two)}\" style='abc: 1'" == build_attributes("href: \"\#{fun(one, two)}\", style: 'abc: 1'}") 122 | end 123 | 124 | test :haml_exceptions do 125 | msg = "tag id is assigned multiple times on line number 1" 126 | assert_raise CalliopeException, msg, fn() -> 127 | parse([[1, "#main", "#another_id"]]) 128 | end 129 | 130 | msg = "Indentation was too deep on line number: 3" 131 | assert_raise CalliopeException, msg, fn() -> 132 | parse([[1, "#main"], 133 | [2, "\t", "%h1", "Calliope"], 134 | [3, "\t\t\t", "%h2", "Indent Too Deep" ]]) 135 | end 136 | 137 | msg = "Unknown filter on line number: 1" 138 | assert_raise CalliopeException, msg, fn() -> 139 | parse([ [1, ":unknown"] ]) 140 | end 141 | end 142 | 143 | test :function_in_attributes do 144 | tokens = [[1, "%a", "{href: '\#{page_path(conn, 1)}'}", " Link"]] 145 | expected = [ 146 | [content: "Link", attributes: "href='\#{page_path(conn, 1)}'", tag: "a", line_number: 1] 147 | ] 148 | assert parse(tokens) == expected 149 | end 150 | 151 | test :dashed_attribute_names do 152 | tokens = [[1, "%li", "(ng-class=\"followees_tab\")"]] 153 | expected = %{attributes: "ng-class=\"followees_tab\"", line_number: 1, tag: "li"} 154 | [result] = parse(tokens) 155 | 156 | assert Enum.into(result, %{}) == expected 157 | end 158 | 159 | test :hash_rocket_attributes do 160 | tokens = [[1, "%p", ".alert", ".alert-info", "{:role => \"alert\"}", "= get_flash(@conn, :info)"]] 161 | expected = %{attributes: "role='alert'", script: " get_flash(@conn, :info)", tag: "p", line_number: 1, classes: ["alert", "alert-info"]} 162 | [result] = parse(tokens) 163 | 164 | assert Enum.into(result, %{}) == expected 165 | end 166 | 167 | test :hash_rocket_script_attribute_exception do 168 | # %script{:src => static_path(@conn, "/js/app.js")} 169 | tokens = [[1, "%script", "{:src => static_path(@conn, \"/js/app.js\")}"]] 170 | assert_raise CalliopeException, ~r/Invalid attribute/, fn -> 171 | parse(tokens) 172 | end 173 | end 174 | 175 | test :hash_script_exception do 176 | assert_raise CalliopeException, ~r/Invalid attribute/, fn -> 177 | parse([[1, ".cls", "#id", "{attr: myfunc(1,2), attr2: \"test\"}", "= one"]]) 178 | end 179 | end 180 | defp parsed_tokens(n), do: Enum.sort line(@parsed_tokens, n) 181 | 182 | defp parsed_line_tokens(tokens), do: Enum.sort parse_line(tokens) 183 | 184 | defp line(list, n), do: Enum.fetch!(list, n) 185 | 186 | defp each_token_with_index(function) do 187 | Enum.each Enum.with_index(@tokens), function 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/calliope/compiler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CalliopeCompilerTest do 2 | use ExUnit.Case 3 | 4 | import Calliope.Compiler 5 | 6 | @ast [ 7 | [ doctype: "!!! 5" ], 8 | [ tag: "section", classes: ["container"], children: [ 9 | [ indent: 1, tag: "h1", children: [ 10 | [ indent: 2, script: "arg"] 11 | ] 12 | ], 13 | [ indent: 1, tag: "h1", comment: "!--", content: "An important inline comment" ], 14 | [content: "", 15 | indent: 1, line_number: 6], 16 | [ indent: 1, id: "main", classes: ["content"], children: [ 17 | [ indent: 2, content: "Welcome to Calliope" ], 18 | [ indent: 2, tag: "br" ] 19 | ] 20 | ], 21 | ], 22 | ], 23 | [ tag: "section", classes: ["container"], children: [ 24 | [ indent: 1, tag: "img", attributes: "src='#'"] 25 | ] 26 | ] 27 | ] 28 | 29 | @html ~s{ 30 |
31 |

32 | <%= arg %> 33 |

34 | 35 | 36 |
37 | Welcome to Calliope 38 |
39 |
40 |
41 |
42 | 43 |
44 | } 45 | 46 | @smart [[smart_script: "for { id, content } <- posts do", children: [ 47 | [indent: 1, tag: "div", children: [[indent: 2, script: "content"]]] 48 | ]]] 49 | 50 | @smart_haml_comments [ 51 | [ tag: "p", content: "foo", children: [ 52 | [ indent: 1, smart_script: "# This would", children: [ 53 | [ indent: 2, content: "Not be"], 54 | [ indent: 2, content: "output"] 55 | ], 56 | ] 57 | ] 58 | ], 59 | [ tag: "p", content: "bar"] 60 | ] 61 | 62 | test :precompile_content do 63 | assert "Hello <%= name %>" == precompile_content("Hello \#{name}") 64 | end 65 | 66 | test :compile_attributes do 67 | assert " id=\"foo\" class=\"bar baz\"" == 68 | compile_attributes([ id: "foo", classes: ["bar", "baz"] ]) 69 | assert " class=\"bar\"" == compile_attributes([ classes: ["bar"] ]) 70 | assert " id=\"foo\"" == compile_attributes([ id: "foo"]) 71 | end 72 | 73 | test :compile_key do 74 | assert " class=\"content\"" == compile_key({ :classes, ["content"] }) 75 | assert " id=\"foo\"" == compile_key({ :id, "foo" }) 76 | end 77 | 78 | test :tag do 79 | refute tag([ foo: "bar" ]) 80 | assert "div" == tag([tag: "div"]) 81 | assert "div" == tag([id: "foo"]) 82 | assert "div" == tag([classes: ["bar"]]) 83 | assert "section" == tag([tag: "section"]) 84 | assert "!!! 5" == tag([doctype: "!!! 5"]) 85 | assert nil == tag([content: "Welcome to Calliope"]) 86 | end 87 | 88 | test :open do 89 | assert "
" == open("", :div) 90 | assert "
" == open("", :section) 91 | assert "" == open("", nil) 92 | 93 | assert "" == open("", "!!! 5") 94 | assert "" == open("", "!!!") 95 | 96 | assert "
" == open(" id=\"foo\" class=\"bar\"", :div) 97 | end 98 | 99 | test :close do 100 | assert "
" == close("div") 101 | assert "
" == close("section") 102 | assert "" == close(nil) 103 | 104 | assert "" == close("br") 105 | assert "" == close("link") 106 | end 107 | 108 | test :compile do 109 | assert ~s{
\n} == compile([[id: "test"]]) 110 | assert ~s{
\n} == compile([[tag: "section", id: "test", classes: ["content"]]]) 111 | 112 | children = [[classes: ["nested"]]] 113 | assert ~s{
\n
\n
\n} == compile([[id: "test", children: children]]) 114 | 115 | assert ~s{content} <> "\n" == compile([[content: "content"]]) 116 | 117 | assert @html == compile(@ast) 118 | end 119 | 120 | test :compile_with_multiline_script do 121 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 122 |

Calliope

123 | <%= for a <- b do %> 124 |
<%= a %>
125 | <% end %>}, "") 126 | 127 | parsed_tokens = [ 128 | [ indent: 1, tag: "h1", content: "Calliope"], 129 | [ indent: 1, smart_script: "for a <- b do", children: [ 130 | [ indent: 2, tag: "div", script: "a"] 131 | ] 132 | ] 133 | ] 134 | 135 | compiled_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, compile(parsed_tokens), "") 136 | 137 | assert expected_results == compiled_results 138 | end 139 | 140 | test :compile_with_cond_evaluation do 141 | expected_results = Regex.replace(~r/(^\s*)|(\s+$)|(\n)/m, ~s{ 142 | <%= cond do %> 143 | <% (1 + 1 == 1) -> %> 144 |

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 = "
\n Test\n
\n" 237 | parsed_tokens = [ 238 | [ indent: 1, tag: "div", line_number: 1, children: [ 239 | [ indent: 2, tag: "b", content: "Test", line_number: 2] 240 | ] 241 | ] 242 | ] 243 | assert expected == compile(parsed_tokens) 244 | end 245 | 246 | test :preserves_indentation_and_new_lines_2 do 247 | expected = "
\n Label:\n Content\n
\nOutside the div\n" 248 | parsed_tokens = [[ line_number: 1, classes: ["simple_div"], children: [ 249 | [content: "Label:", tag: "b", indent: 1, line_number: 2], 250 | [content: "Content", indent: 1, line_number: 3]]], 251 | [content: "Outside the div", line_number: 4]] 252 | assert compile(parsed_tokens) == expected 253 | end 254 | 255 | @expected ~s[<%= form_for @changeset, @action, fn f -> %> 256 |
257 | 258 | <% end %>] 259 | 260 | test :render_anonymous_functions do 261 | parsed_tokens = [ 262 | [smart_script: "form_for @changeset, @action, fn f ->", line_number: 1, 263 | children: [[line_number: 2, indent: 1, classes: ["test"]]]] 264 | ] 265 | assert compile(parsed_tokens) == @expected 266 | end 267 | 268 | @expected ~s[<%= form_for @changeset, @action, fn(f) -> %> 269 |
270 | 271 | <% end %>] 272 | 273 | test :render_anonymous_function_parens do 274 | parsed_tokens = [ 275 | [smart_script: "form_for @changeset, @action, fn(f) ->", line_number: 1, 276 | children: [[line_number: 2, indent: 1, classes: ["test"]]]] 277 | ] 278 | assert compile(parsed_tokens) == @expected 279 | end 280 | 281 | @expected ~s[<%= form_for(@changeset, @action, fn(f) -> %> 282 |
283 | 284 | <% end) %>] 285 | 286 | test :render_anonymous_functions_parens_2 do 287 | parsed_tokens = [ 288 | [smart_script: "form_for(@changeset, @action, fn(f) ->", line_number: 1, 289 | children: [[line_number: 2, indent: 1, classes: ["test"]]]] 290 | ] 291 | assert compile(parsed_tokens) == @expected 292 | end 293 | end 294 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------