From f2ea47f1151a227a75b0ee0784cbe37c6464a976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 21 Feb 2012 19:50:44 +0100 Subject: [PATCH 01/20] Tokenizer is ready. --- lib/eex/tokenizer.ex | 126 +++++++++++++++++++++++++++++ test/elixir/eex/tokenizer_test.exs | 47 +++++++++++ 2 files changed, 173 insertions(+) create mode 100644 lib/eex/tokenizer.ex create mode 100644 test/elixir/eex/tokenizer_test.exs diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex new file mode 100644 index 00000000000..bb15435e159 --- /dev/null +++ b/lib/eex/tokenizer.ex @@ -0,0 +1,126 @@ +defmodule EEx::Tokenizer do + @doc """ + Tokenizes the given char list. It returns 4 tokens as result: + + * { :text, contents } + * { :expr, marker, contents} + * { :start_expr, marker, contents} + * { :end_expr, marker, contents} + + """ + def tokenize(list) do + List.reverse(tokenize(list, [], [])) + end + + defp tokenize('<%' ++ t, buffer, acc) do + { marker, t } = retrieve_marker(t) + { expr, rest } = tokenize_expr t, [] + + token = tip_expr_token_name(expr) + expr = List.reverse(expr) + + # If it isn't a start or end token, it may be a middle token. + if token == :expr, do: + token = middle_expr_token_name(expr) + + acc = tokenize_text(buffer, acc) + tokenize rest, [], [ { token, marker, expr } | acc] + end + + defp tokenize([h|t], buffer, acc) do + tokenize t, [h|buffer], acc + end + + defp tokenize([], buffer, acc) do + tokenize_text(buffer, acc) + end + + # Retrieve marker for <% + + defp retrieve_marker('=' ++ t) do + { '=', t } + end + + defp retrieve_marker(t) do + { '', t } + end + + # Tokenize an expression until we find %> + + defp tokenize_expr('%>' ++ t, buffer) do + { buffer, t } + end + + defp tokenize_expr([h|t], buffer) do + tokenize_expr t, [h|buffer] + end + + # Receive an expression content and check + # if it is a start or an end token. + # Start tokens finish with `do` or `->` + # while end tokens contain only the end word. + + defp tip_expr_token_name([h|t]) when h == ?\s orelse h == ?\t do + tip_expr_token_name(t) + end + + defp tip_expr_token_name('od' ++ [h|_]) when h == ?\s orelse h == ?\t orelse h == ?) do + :start_expr + end + + defp tip_expr_token_name('>-' ++ [h|_]) when h == ?\s orelse h == ?\t orelse h == ?) do + :start_expr + end + + defp tip_expr_token_name('dne' ++ t) do + if only_spaces?(t), do: :end_expr, else: :expr + end + + defp tip_expr_token_name(_) do + :expr + end + + # Receive an expression contents and see if it matches + # a key-value arg syntax, like elsif: foo. + + defp middle_expr_token_name([h|t]) when h == ?\s orelse h == ?\t do + middle_expr_token_name(t) + end + + defp middle_expr_token_name([h|t]) when h >= ?a andalso h <= ?z do + if valid_key_identifier?(t), do: :middle_expr, else: :expr + end + + defp middle_expr_token_name(_) do + :expr + end + + defp valid_key_identifier?([h|t]) \ + when h >= ?a andalso h <= ?z \ + when h >= ?A andalso h <= ?Z \ + when h >= ?0 andalso h <= ?9 do + valid_key_identifier?(t) + end + + defp valid_key_identifier?([?:|_]) do + true + end + + defp valid_key_identifier?(_) do + false + end + + defp only_spaces?([h|t]) when h == ?\s orelse h == ?\t, do: only_spaces?(t) + defp only_spaces?(other), do: other == [] + + # Tokenize the buffered text by appending + # it to the given accumulator. + + defp tokenize_text([], acc) do + acc + end + + defp tokenize_text(buffer, acc) do + [{ :text, List.reverse buffer } | acc] + end +end \ No newline at end of file diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs new file mode 100644 index 00000000000..2cb60ebcd60 --- /dev/null +++ b/test/elixir/eex/tokenizer_test.exs @@ -0,0 +1,47 @@ +Code.require_file "../../test_helper", __FILE__ + +defmodule EEx::TokenizerTest do + use ExUnit::Case + require EEx::Tokenizer, as: T + + test "simple strings" do + assert_equal [ { :text, 'foo' } ], T.tokenize('foo') + end + + test "strings with embedded code" do + assert_equal [ { :text, 'foo ' }, { :expr, [], ' bar ' }], T.tokenize('foo <% bar %>') + end + + test "strings with embedded equals code" do + assert_equal [ { :text, 'foo ' }, { :expr, '=', ' bar ' }], T.tokenize('foo <%= bar %>') + end + + test "strings with embedded do end" do + assert_equal [ + { :text, 'foo ' }, + { :start_expr, '', ' if true do ' }, + { :text, 'bar' }, + { :end_expr, '', ' end ' } + ], T.tokenize('foo <% if true do %>bar<% end %>') + end + + test "strings with embedded -> end" do + assert_equal [ + { :text, 'foo ' }, + { :start_expr, '', ' if(true)-> ' }, + { :text, 'bar' }, + { :end_expr, '', ' end ' } + ], T.tokenize('foo <% if(true)-> %>bar<% end %>') + end + + test "strings with embedded key-value blocks" do + assert_equal [ + { :text, 'foo ' }, + { :start_expr, '', ' if true do ' }, + { :text, 'bar' }, + { :middle_expr, '', ' elsif: false ' }, + { :text, 'baz' }, + { :end_expr, '', ' end ' } + ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>') + end +end \ No newline at end of file From 5964f7ce671756974cf01663fc03d9b759e849e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 17:18:17 -0200 Subject: [PATCH 02/20] Make tokenizer accepts a binary --- lib/eex/tokenizer.ex | 14 ++++++++--- test/elixir/eex/tokenizer_test.exs | 40 ++++++++++++++++-------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index bb15435e159..7bbde17f636 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -1,13 +1,19 @@ defmodule EEx::Tokenizer do + # TODO: Add errors scenarios + @doc """ Tokenizes the given char list. It returns 4 tokens as result: - + * { :text, contents } * { :expr, marker, contents} * { :start_expr, marker, contents} * { :end_expr, marker, contents} """ + def tokenize(bin) when is_binary(bin) do + tokenize(binary_to_list(bin)) + end + def tokenize(list) do List.reverse(tokenize(list, [], [])) end @@ -24,7 +30,7 @@ defmodule EEx::Tokenizer do token = middle_expr_token_name(expr) acc = tokenize_text(buffer, acc) - tokenize rest, [], [ { token, marker, expr } | acc] + tokenize rest, [], [ { token, marker, list_to_binary(expr) } | acc] end defp tokenize([h|t], buffer, acc) do @@ -121,6 +127,6 @@ defmodule EEx::Tokenizer do end defp tokenize_text(buffer, acc) do - [{ :text, List.reverse buffer } | acc] + [{ :text, list_to_binary(List.reverse(buffer)) } | acc] end -end \ No newline at end of file +end diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs index 2cb60ebcd60..cabfc8dae66 100644 --- a/test/elixir/eex/tokenizer_test.exs +++ b/test/elixir/eex/tokenizer_test.exs @@ -4,44 +4,48 @@ defmodule EEx::TokenizerTest do use ExUnit::Case require EEx::Tokenizer, as: T + test "simple chars lists" do + assert_equal [ { :text, "foo" } ], T.tokenize('foo') + end + test "simple strings" do - assert_equal [ { :text, 'foo' } ], T.tokenize('foo') + assert_equal [ { :text, "foo" } ], T.tokenize("foo") end test "strings with embedded code" do - assert_equal [ { :text, 'foo ' }, { :expr, [], ' bar ' }], T.tokenize('foo <% bar %>') + assert_equal [ { :text, "foo " }, { :expr, [], " bar " }], T.tokenize('foo <% bar %>') end test "strings with embedded equals code" do - assert_equal [ { :text, 'foo ' }, { :expr, '=', ' bar ' }], T.tokenize('foo <%= bar %>') + assert_equal [ { :text, "foo " }, { :expr, '=', " bar " }], T.tokenize('foo <%= bar %>') end test "strings with embedded do end" do assert_equal [ - { :text, 'foo ' }, - { :start_expr, '', ' if true do ' }, - { :text, 'bar' }, - { :end_expr, '', ' end ' } + { :text, "foo " }, + { :start_expr, '', " if true do " }, + { :text, "bar" }, + { :end_expr, '', " end " } ], T.tokenize('foo <% if true do %>bar<% end %>') end test "strings with embedded -> end" do assert_equal [ - { :text, 'foo ' }, - { :start_expr, '', ' if(true)-> ' }, - { :text, 'bar' }, - { :end_expr, '', ' end ' } + { :text, "foo " }, + { :start_expr, '', " if(true)-> " }, + { :text, "bar" }, + { :end_expr, '', " end " } ], T.tokenize('foo <% if(true)-> %>bar<% end %>') end test "strings with embedded key-value blocks" do assert_equal [ - { :text, 'foo ' }, - { :start_expr, '', ' if true do ' }, - { :text, 'bar' }, - { :middle_expr, '', ' elsif: false ' }, - { :text, 'baz' }, - { :end_expr, '', ' end ' } + { :text, "foo " }, + { :start_expr, '', " if true do " }, + { :text, "bar" }, + { :middle_expr, '', " elsif: false " }, + { :text, "baz" }, + { :end_expr, '', " end " } ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>') end -end \ No newline at end of file +end From a54b99765e10e1541cf62441922fd56d51950627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 17:33:49 -0200 Subject: [PATCH 03/20] Fix eval_quoted documentation --- lib/code.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/code.ex b/lib/code.ex index 810945c3182..e5d11ca864a 100644 --- a/lib/code.ex +++ b/lib/code.ex @@ -33,8 +33,8 @@ defmodule Code do # # ## Examples # - # contents = quote do: a + b - # Code.eval_quoted contents, [a: 1, b: 2], __FILE__, __LINE__ # => 3 + # contents = quote hygiene: false, do: a + b + # Code.eval_quoted contents, [a: 1, b: 2], __FILE__, __LINE__ # => { 3, [ {:a,1},{:b,2} ] } # def eval_quoted(quoted, binding, filename, line) do Erlang.elixir.eval_quoted [quoted], binding, line, to_char_list(filename) @@ -134,4 +134,4 @@ defmodule Code do defp server_call(args) do Erlang.gen_server.call(:elixir_code_server, args) end -end \ No newline at end of file +end From 183aa673681bd9931139f9f297de4a739a436a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 17:34:15 -0200 Subject: [PATCH 04/20] EEx compile a simple string --- lib/eex.ex | 21 +++++++++++++++++++++ lib/eex/engine.ex | 7 +++++++ test/elixir/eex_test.exs | 15 +++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 lib/eex.ex create mode 100644 lib/eex/engine.ex create mode 100644 test/elixir/eex_test.exs diff --git a/lib/eex.ex b/lib/eex.ex new file mode 100644 index 00000000000..ea21127ee4b --- /dev/null +++ b/lib/eex.ex @@ -0,0 +1,21 @@ +defmodule EEx do + def compile(source, engine // EEx::Engine) do + EEx::Compiler.compile(source, engine) + end +end + +defmodule EEx::Compiler do + def compile(source, engine) do + tokens = EEx::Tokenizer.tokenize(source) + generate_buffer(tokens, engine, "") + end + + defp generate_buffer([{ :text, chars }|t], engine, buffer) do + buffer = engine.handle_text(buffer, chars) + generate_buffer(t, engine, buffer) + end + + defp generate_buffer([], _engine, buffer) do + buffer + end +end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex new file mode 100644 index 00000000000..950e14d3f77 --- /dev/null +++ b/lib/eex/engine.ex @@ -0,0 +1,7 @@ +defmodule EEx::Engine do + def handle_text(buffer, text) do + quote do + unquote(buffer) <> unquote(text) + end + end +end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs new file mode 100644 index 00000000000..d774fb7c9c2 --- /dev/null +++ b/test/elixir/eex_test.exs @@ -0,0 +1,15 @@ +Code.require_file "../test_helper", __FILE__ + +defmodule EExTest do + use ExUnit::Case + + test "compile simple string" do + assert_eval "foo bar", "foo bar" + end + + defp assert_eval(expected, atual) do + compiled = EEx.compile(atual) + { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) + assert_equal expected, result + end +end From 6f158ba1f40d33367573a61006aa6aacb5e19ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 17:55:32 -0200 Subject: [PATCH 05/20] Expressions should not be a string --- lib/eex/tokenizer.ex | 2 +- test/elixir/eex/tokenizer_test.exs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index 7bbde17f636..ff7cf896e6b 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -30,7 +30,7 @@ defmodule EEx::Tokenizer do token = middle_expr_token_name(expr) acc = tokenize_text(buffer, acc) - tokenize rest, [], [ { token, marker, list_to_binary(expr) } | acc] + tokenize rest, [], [ { token, marker, expr } | acc] end defp tokenize([h|t], buffer, acc) do diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs index cabfc8dae66..ae1467b2159 100644 --- a/test/elixir/eex/tokenizer_test.exs +++ b/test/elixir/eex/tokenizer_test.exs @@ -13,39 +13,39 @@ defmodule EEx::TokenizerTest do end test "strings with embedded code" do - assert_equal [ { :text, "foo " }, { :expr, [], " bar " }], T.tokenize('foo <% bar %>') + assert_equal [ { :text, "foo " }, { :expr, [], ' bar ' }], T.tokenize('foo <% bar %>') end test "strings with embedded equals code" do - assert_equal [ { :text, "foo " }, { :expr, '=', " bar " }], T.tokenize('foo <%= bar %>') + assert_equal [ { :text, "foo " }, { :expr, '=', ' bar ' }], T.tokenize('foo <%= bar %>') end test "strings with embedded do end" do assert_equal [ { :text, "foo " }, - { :start_expr, '', " if true do " }, + { :start_expr, '', ' if true do ' }, { :text, "bar" }, - { :end_expr, '', " end " } + { :end_expr, '', ' end ' } ], T.tokenize('foo <% if true do %>bar<% end %>') end test "strings with embedded -> end" do assert_equal [ { :text, "foo " }, - { :start_expr, '', " if(true)-> " }, + { :start_expr, '', ' if(true)-> ' }, { :text, "bar" }, - { :end_expr, '', " end " } + { :end_expr, '', ' end ' } ], T.tokenize('foo <% if(true)-> %>bar<% end %>') end test "strings with embedded key-value blocks" do assert_equal [ { :text, "foo " }, - { :start_expr, '', " if true do " }, + { :start_expr, '', ' if true do ' }, { :text, "bar" }, - { :middle_expr, '', " elsif: false " }, + { :middle_expr, '', ' elsif: false ' }, { :text, "baz" }, - { :end_expr, '', " end " } + { :end_expr, '', ' end ' } ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>') end end From 15bf69322f9a42094adb662a31a630e0f188a242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 18:01:21 -0200 Subject: [PATCH 06/20] Test compile with an embedded code --- lib/eex.ex | 7 +++++++ lib/eex/engine.ex | 6 ++++++ test/elixir/eex_test.exs | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/lib/eex.ex b/lib/eex.ex index ea21127ee4b..61dc963c7b4 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -15,6 +15,13 @@ defmodule EEx::Compiler do generate_buffer(t, engine, buffer) end + # TODO: use line and filename + defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer) do + expr = { :__BLOCK__, 0, Erlang.elixir_translator.forms(chars, 1, 'nofile') } + buffer = engine.handle_expr(buffer, mark, expr) + generate_buffer(t, engine, buffer) + end + defp generate_buffer([], _engine, buffer) do buffer end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index 950e14d3f77..103d3bb722e 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -4,4 +4,10 @@ defmodule EEx::Engine do unquote(buffer) <> unquote(text) end end + + def handle_expr(buffer, '=', expr) do + quote do + unquote(buffer) <> to_binary(unquote(expr)) + end + end end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index d774fb7c9c2..f1b449a92e0 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -7,6 +7,10 @@ defmodule EExTest do assert_eval "foo bar", "foo bar" end + test "compile with embedded" do + assert_eval "foo bar", "foo <%= :bar %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From 42030d1eff1ad96aa7cd020796a9e88de8d3a0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 18:12:26 -0200 Subject: [PATCH 07/20] Add tests to EEx::Engine --- test/elixir/eex/engine_test.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/elixir/eex/engine_test.exs diff --git a/test/elixir/eex/engine_test.exs b/test/elixir/eex/engine_test.exs new file mode 100644 index 00000000000..c88d1157d61 --- /dev/null +++ b/test/elixir/eex/engine_test.exs @@ -0,0 +1,14 @@ +Code.require_file "../../test_helper", __FILE__ + +defmodule EEx::EngineTest do + use ExUnit::Case + require EEx::Engine, as: E + + test "handle text" do + assert_equal {:"<>", 0, [[], "foo"]}, E.handle_text([], "foo") + end + + test "handle equal expression" do + assert_equal {:"<>", 0, [[],{:to_binary,0,[:foo]}]}, E.handle_expr([], '=', :foo) + end +end From 30eee3dc6163a4ffcd2477983c4d6eb1e21314e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 21 Feb 2012 20:56:52 -0200 Subject: [PATCH 08/20] Use blank string as buffer --- test/elixir/eex/engine_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/elixir/eex/engine_test.exs b/test/elixir/eex/engine_test.exs index c88d1157d61..350a8965507 100644 --- a/test/elixir/eex/engine_test.exs +++ b/test/elixir/eex/engine_test.exs @@ -5,10 +5,10 @@ defmodule EEx::EngineTest do require EEx::Engine, as: E test "handle text" do - assert_equal {:"<>", 0, [[], "foo"]}, E.handle_text([], "foo") + assert_equal {:"<>", 0, ["", "foo"]}, E.handle_text("", "foo") end test "handle equal expression" do - assert_equal {:"<>", 0, [[],{:to_binary,0,[:foo]}]}, E.handle_expr([], '=', :foo) + assert_equal {:"<>", 0, ["",{:to_binary,0,[:foo]}]}, E.handle_expr("", '=', :foo) end end From 6711f6de3e4eb4a9a7bc99036671e8a0a349681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 24 Feb 2012 11:16:43 -0200 Subject: [PATCH 09/20] EEx now compile blocks --- lib/eex.ex | 47 +++++++++++++++++++++++++++++++++++----- lib/eex/engine.ex | 2 +- test/elixir/eex_test.exs | 8 +++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index 61dc963c7b4..032413133af 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -7,22 +7,57 @@ end defmodule EEx::Compiler do def compile(source, engine) do tokens = EEx::Tokenizer.tokenize(source) - generate_buffer(tokens, engine, "") + generate_buffer(tokens, engine, "", []) end - defp generate_buffer([{ :text, chars }|t], engine, buffer) do + defp generate_buffer([{ :text, chars }|t], engine, buffer, scope) do buffer = engine.handle_text(buffer, chars) - generate_buffer(t, engine, buffer) + generate_buffer(t, engine, buffer, scope) end # TODO: use line and filename - defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer) do + defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer, scope) do expr = { :__BLOCK__, 0, Erlang.elixir_translator.forms(chars, 1, 'nofile') } buffer = engine.handle_expr(buffer, mark, expr) - generate_buffer(t, engine, buffer) + generate_buffer(t, engine, buffer, scope) end - defp generate_buffer([], _engine, buffer) do + defp generate_buffer([{ :start_expr, mark, chars }|t], engine, buffer, scope) do + { contents, t } = generate_buffer(t, engine, "", [chars|scope]) + buffer = engine.handle_expr(buffer, mark, contents) + generate_buffer(t, engine, buffer, scope) + end + + defp generate_buffer([{ :end_expr, _mark, chars }|t], _engine, buffer, [current|_]) do + tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(current ++ '__EEX__(1)' ++ chars, 1, 'nofile') } + dict = Orddict.put([], 1, buffer) + buffer = insert_quotes(tuples, dict) + { buffer, t } + end + + defp generate_buffer([], _engine, buffer, _scope) do buffer end + + #### + + def insert_quotes( { :__EEX__, _, [key] }, dict) do + Orddict.get(dict, key) + end + + def insert_quotes({ left, line, right }, dict) do + { insert_quotes(left, dict), line, insert_quotes(right, dict) } + end + + def insert_quotes({ left, right }, dict) do + { insert_quotes(left, dict), insert_quotes(right, dict) } + end + + def insert_quotes(list, dict) when is_list(list) do + Enum.map list, insert_quotes(&1, dict) + end + + def insert_quotes(other, _dict) do + other + end end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index 103d3bb722e..e7718c8a8ea 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -5,7 +5,7 @@ defmodule EEx::Engine do end end - def handle_expr(buffer, '=', expr) do + def handle_expr(buffer, _, expr) do quote do unquote(buffer) <> to_binary(unquote(expr)) end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index f1b449a92e0..18bb62e1d55 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -11,6 +11,14 @@ defmodule EExTest do assert_eval "foo bar", "foo <%= :bar %>" end + test "compile with embedded do end" do + assert_eval "foo bar", "foo <% if true do %>bar<% end %>" + end + + test "compile with embedded do end and eval the expression" do + assert_eval "foo ", "foo <% if false do %>bar<% end %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From 509a64f953cdab9b420e80010f3e0b4c1ce97d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 24 Feb 2012 12:03:41 -0200 Subject: [PATCH 10/20] Do not evaluate simple expressions --- lib/eex.ex | 4 ++-- lib/eex/engine.ex | 6 +++++- test/elixir/eex_test.exs | 8 ++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index 032413133af..4681736b1b8 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -22,9 +22,9 @@ defmodule EEx::Compiler do generate_buffer(t, engine, buffer, scope) end - defp generate_buffer([{ :start_expr, mark, chars }|t], engine, buffer, scope) do + defp generate_buffer([{ :start_expr, _mark, chars }|t], engine, buffer, scope) do { contents, t } = generate_buffer(t, engine, "", [chars|scope]) - buffer = engine.handle_expr(buffer, mark, contents) + buffer = engine.handle_expr(buffer, '=', contents) generate_buffer(t, engine, buffer, scope) end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index e7718c8a8ea..652cf74a787 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -5,9 +5,13 @@ defmodule EEx::Engine do end end - def handle_expr(buffer, _, expr) do + def handle_expr(buffer, '=', expr) do quote do unquote(buffer) <> to_binary(unquote(expr)) end end + + def handle_expr(buffer, '', _) do + buffer + end end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index 18bb62e1d55..5dd74f75f8c 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -19,6 +19,14 @@ defmodule EExTest do assert_eval "foo ", "foo <% if false do %>bar<% end %>" end + test "compile with embedded do end and nested print expression" do + assert_eval "foo bar", "foo <% if true do %><%= :bar %><% end %>" + end + + test "compile with embedded do end and nested expression" do + assert_eval "foo ", "foo <% if true do %><% 1 + 2 %><% end %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From e902800e0c0dd3e7c5e20d9ba2a6d4b2aae2e52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 24 Feb 2012 19:08:23 -0200 Subject: [PATCH 11/20] Consider middle expression too --- lib/eex.ex | 29 +++++++++++++++++------------ lib/eex/engine.ex | 17 +++++++++-------- test/elixir/eex_test.exs | 8 ++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index 4681736b1b8..e5b25b7c637 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -7,35 +7,40 @@ end defmodule EEx::Compiler do def compile(source, engine) do tokens = EEx::Tokenizer.tokenize(source) - generate_buffer(tokens, engine, "", []) + generate_buffer(tokens, engine, "", [], []) end - defp generate_buffer([{ :text, chars }|t], engine, buffer, scope) do + defp generate_buffer([{ :text, chars }|t], engine, buffer, scope, dict) do buffer = engine.handle_text(buffer, chars) - generate_buffer(t, engine, buffer, scope) + generate_buffer(t, engine, buffer, scope, dict) end # TODO: use line and filename - defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer, scope) do + defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer, scope, dict) do expr = { :__BLOCK__, 0, Erlang.elixir_translator.forms(chars, 1, 'nofile') } buffer = engine.handle_expr(buffer, mark, expr) - generate_buffer(t, engine, buffer, scope) + generate_buffer(t, engine, buffer, scope, dict) end - defp generate_buffer([{ :start_expr, _mark, chars }|t], engine, buffer, scope) do - { contents, t } = generate_buffer(t, engine, "", [chars|scope]) + defp generate_buffer([{ :start_expr, _, chars }|t], engine, buffer, scope, _dict) do + { contents, t } = generate_buffer(t, engine, "", [chars|scope], []) buffer = engine.handle_expr(buffer, '=', contents) - generate_buffer(t, engine, buffer, scope) + generate_buffer(t, engine, buffer, scope, []) end - defp generate_buffer([{ :end_expr, _mark, chars }|t], _engine, buffer, [current|_]) do - tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(current ++ '__EEX__(1)' ++ chars, 1, 'nofile') } - dict = Orddict.put([], 1, buffer) + defp generate_buffer([{ :middle_expr, _, chars }|t], engine, buffer, [current|scope], dict) do + { wrapped, dict } = engine.wrap_expr(current, buffer, chars, dict) + generate_buffer(t, engine, "", [wrapped|scope], dict) + end + + defp generate_buffer([{ :end_expr, _, chars }|t], engine, buffer, [current|_], dict) do + { wrapped, dict } = engine.wrap_expr(current, buffer, chars, dict) + tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(wrapped, 1, 'nofile') } buffer = insert_quotes(tuples, dict) { buffer, t } end - defp generate_buffer([], _engine, buffer, _scope) do + defp generate_buffer([], _engine, buffer, _scope, _dict) do buffer end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index 652cf74a787..8bc8eb09fbc 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -1,17 +1,18 @@ defmodule EEx::Engine do def handle_text(buffer, text) do - quote do - unquote(buffer) <> unquote(text) - end + quote do: unquote(buffer) <> unquote(text) end def handle_expr(buffer, '=', expr) do - quote do - unquote(buffer) <> to_binary(unquote(expr)) - end + quote do: unquote(buffer) <> to_binary(unquote(expr)) end + def handle_expr(buffer, '', _), do: buffer - def handle_expr(buffer, '', _) do - buffer + def wrap_expr(current, buffer, chars, dict) do + key = length(dict) + placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');' + dict = Orddict.put(dict, key, buffer) + + { current ++ placeholder ++ chars, dict } end end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index 5dd74f75f8c..fb60569d715 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -27,6 +27,14 @@ defmodule EExTest do assert_eval "foo ", "foo <% if true do %><% 1 + 2 %><% end %>" end + test "compile with embedded middle expression" do + assert_eval "foo bar", "foo <% if true do %>bar<% else: %>baz<% end %>" + end + + test "compile with embedded middle expression and eval the expression" do + assert_eval "foo baz", "foo <% if false do %>bar<% else: %>baz<% end %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From f2596491e3b8f4c07c6e1cd7aef8a4080f338b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Feb 2012 16:53:56 -0300 Subject: [PATCH 12/20] Update to last Elixir --- lib/eex/tokenizer.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index ff7cf896e6b..08960a9e28d 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -66,15 +66,15 @@ defmodule EEx::Tokenizer do # Start tokens finish with `do` or `->` # while end tokens contain only the end word. - defp tip_expr_token_name([h|t]) when h == ?\s orelse h == ?\t do + defp tip_expr_token_name([h|t]) when h == ?\s or h == ?\t do tip_expr_token_name(t) end - defp tip_expr_token_name('od' ++ [h|_]) when h == ?\s orelse h == ?\t orelse h == ?) do + defp tip_expr_token_name('od' ++ [h|_]) when h == ?\s or h == ?\t or h == ?) do :start_expr end - defp tip_expr_token_name('>-' ++ [h|_]) when h == ?\s orelse h == ?\t orelse h == ?) do + defp tip_expr_token_name('>-' ++ [h|_]) when h == ?\s or h == ?\t or h == ?) do :start_expr end @@ -89,11 +89,11 @@ defmodule EEx::Tokenizer do # Receive an expression contents and see if it matches # a key-value arg syntax, like elsif: foo. - defp middle_expr_token_name([h|t]) when h == ?\s orelse h == ?\t do + defp middle_expr_token_name([h|t]) when h == ?\s or h == ?\t do middle_expr_token_name(t) end - defp middle_expr_token_name([h|t]) when h >= ?a andalso h <= ?z do + defp middle_expr_token_name([h|t]) when h >= ?a and h <= ?z do if valid_key_identifier?(t), do: :middle_expr, else: :expr end @@ -102,9 +102,9 @@ defmodule EEx::Tokenizer do end defp valid_key_identifier?([h|t]) \ - when h >= ?a andalso h <= ?z \ - when h >= ?A andalso h <= ?Z \ - when h >= ?0 andalso h <= ?9 do + when h >= ?a and h <= ?z \ + when h >= ?A and h <= ?Z \ + when h >= ?0 and h <= ?9 do valid_key_identifier?(t) end @@ -116,7 +116,7 @@ defmodule EEx::Tokenizer do false end - defp only_spaces?([h|t]) when h == ?\s orelse h == ?\t, do: only_spaces?(t) + defp only_spaces?([h|t]) when h == ?\s or h == ?\t, do: only_spaces?(t) defp only_spaces?(other), do: other == [] # Tokenize the buffered text by appending From af5154d4d3a7e7bb09ce72b6a051dd9401f60576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Feb 2012 17:52:16 -0300 Subject: [PATCH 13/20] Do not ignore expression when is not a print expression --- lib/eex/engine.ex | 8 +++++++- test/elixir/eex_test.exs | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index 8bc8eb09fbc..b4001020015 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -6,7 +6,13 @@ defmodule EEx::Engine do def handle_expr(buffer, '=', expr) do quote do: unquote(buffer) <> to_binary(unquote(expr)) end - def handle_expr(buffer, '', _), do: buffer + + def handle_expr(buffer, '', expr) do + quote do + unquote(expr) + unquote(buffer) + end + end def wrap_expr(current, buffer, chars, dict) do key = length(dict) diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index fb60569d715..aaec2be1543 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -23,8 +23,9 @@ defmodule EExTest do assert_eval "foo bar", "foo <% if true do %><%= :bar %><% end %>" end - test "compile with embedded do end and nested expression" do - assert_eval "foo ", "foo <% if true do %><% 1 + 2 %><% end %>" + test "compile with embedded do end and nested expressions" do + assert_eval "foo bar baz", "foo <% if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>" + assert_equal 1, Process.get(:eex_text) end test "compile with embedded middle expression" do From 44f841ad72fdda92caab48ff8a4a9cd9505c8a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Feb 2012 17:55:08 -0300 Subject: [PATCH 14/20] Move down the wrap_expr to the Compiler --- lib/eex.ex | 16 +++++++++++++--- lib/eex/engine.ex | 8 -------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index e5b25b7c637..49a63cd88a6 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -29,12 +29,12 @@ defmodule EEx::Compiler do end defp generate_buffer([{ :middle_expr, _, chars }|t], engine, buffer, [current|scope], dict) do - { wrapped, dict } = engine.wrap_expr(current, buffer, chars, dict) + { wrapped, dict } = wrap_expr(current, buffer, chars, dict) generate_buffer(t, engine, "", [wrapped|scope], dict) end - defp generate_buffer([{ :end_expr, _, chars }|t], engine, buffer, [current|_], dict) do - { wrapped, dict } = engine.wrap_expr(current, buffer, chars, dict) + defp generate_buffer([{ :end_expr, _, chars }|t], _engine, buffer, [current|_], dict) do + { wrapped, dict } = wrap_expr(current, buffer, chars, dict) tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(wrapped, 1, 'nofile') } buffer = insert_quotes(tuples, dict) { buffer, t } @@ -46,6 +46,16 @@ defmodule EEx::Compiler do #### + def wrap_expr(current, buffer, chars, dict) do + key = length(dict) + placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');' + dict = Orddict.put(dict, key, buffer) + + { current ++ placeholder ++ chars, dict } + end + + ### + def insert_quotes( { :__EEX__, _, [key] }, dict) do Orddict.get(dict, key) end diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index b4001020015..f9bda42bcb4 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -13,12 +13,4 @@ defmodule EEx::Engine do unquote(buffer) end end - - def wrap_expr(current, buffer, chars, dict) do - key = length(dict) - placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');' - dict = Orddict.put(dict, key, buffer) - - { current ++ placeholder ++ chars, dict } - end end From 08a2606058d355d49f8fc6fdf364f5cd6712c304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Feb 2012 17:59:47 -0300 Subject: [PATCH 15/20] Add tests to nested expressions --- test/elixir/eex_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index aaec2be1543..f282399d946 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -36,6 +36,14 @@ defmodule EExTest do assert_eval "foo baz", "foo <% if false do %>bar<% else: %>baz<% end %>" end + test "compile with nested start expression" do + assert_eval "foo bar", "foo <% if true do %><% if true do %>bar<% end %><% end %>" + end + + test "compile with nested middle expression" do + assert_eval "foo baz", "foo <% if true do %><% if false do %>bar<% else: %>baz<% end %><% end %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From 6f8606d79012dccc0ccfd07c70dc50e6f3b54c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 27 Feb 2012 18:40:03 -0300 Subject: [PATCH 16/20] Make EEx works with variables and requide code --- lib/eex/engine.ex | 9 +++++++-- test/elixir/eex/engine_test.exs | 14 -------------- test/elixir/eex_test.exs | 8 ++++++++ 3 files changed, 15 insertions(+), 16 deletions(-) delete mode 100644 test/elixir/eex/engine_test.exs diff --git a/lib/eex/engine.ex b/lib/eex/engine.ex index f9bda42bcb4..46593b9dabf 100644 --- a/lib/eex/engine.ex +++ b/lib/eex/engine.ex @@ -4,13 +4,18 @@ defmodule EEx::Engine do end def handle_expr(buffer, '=', expr) do - quote do: unquote(buffer) <> to_binary(unquote(expr)) + quote do + tmp_1 = unquote(buffer) + tmp_2 = to_binary(unquote(expr)) + tmp_1 <> tmp_2 + end end def handle_expr(buffer, '', expr) do quote do + tmp = unquote(buffer) unquote(expr) - unquote(buffer) + tmp end end end diff --git a/test/elixir/eex/engine_test.exs b/test/elixir/eex/engine_test.exs deleted file mode 100644 index 350a8965507..00000000000 --- a/test/elixir/eex/engine_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -Code.require_file "../../test_helper", __FILE__ - -defmodule EEx::EngineTest do - use ExUnit::Case - require EEx::Engine, as: E - - test "handle text" do - assert_equal {:"<>", 0, ["", "foo"]}, E.handle_text("", "foo") - end - - test "handle equal expression" do - assert_equal {:"<>", 0, ["",{:to_binary,0,[:foo]}]}, E.handle_expr("", '=', :foo) - end -end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index f282399d946..49d8000e7cc 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -44,6 +44,14 @@ defmodule EExTest do assert_eval "foo baz", "foo <% if true do %><% if false do %>bar<% else: %>baz<% end %><% end %>" end + test "compile with defined variable" do + assert_eval "foo 1", "foo <% bar = 1 %><%= bar %>" + end + + test "compile with require code" do + assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1,2,3], \",\" %>" + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From c795f09f550364986be2f15948deb2393f4d1421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 28 Feb 2012 11:29:59 -0300 Subject: [PATCH 17/20] Error handler --- lib/eex.ex | 12 +++++++++++- lib/eex/tokenizer.ex | 6 ++++++ test/elixir/eex/tokenizer_test.exs | 6 ++++++ test/elixir/eex_test.exs | 28 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/eex.ex b/lib/eex.ex index 49a63cd88a6..5d50be73783 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -4,6 +4,8 @@ defmodule EEx do end end +defexception EEx::SyntaxError, message: nil + defmodule EEx::Compiler do def compile(source, engine) do tokens = EEx::Tokenizer.tokenize(source) @@ -40,10 +42,18 @@ defmodule EEx::Compiler do { buffer, t } end - defp generate_buffer([], _engine, buffer, _scope, _dict) do + defp generate_buffer([{ :end_expr, _, chars }|_], _engine, _buffer, [], _dict) do + raise SyntaxError, message: "unexpected token: #{inspect chars}" + end + + defp generate_buffer([], _engine, buffer, [], _dict) do buffer end + defp generate_buffer([], _engine, _buffer, _scope, _dict) do + raise SyntaxError, message: "undetermined end of string" + end + #### def wrap_expr(current, buffer, chars, dict) do diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index 08960a9e28d..c0ea1756945 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -61,6 +61,12 @@ defmodule EEx::Tokenizer do tokenize_expr t, [h|buffer] end + # Raise an error if the %> is not found + + defp tokenize_expr([], buffer) do + raise EEx::SyntaxError, message: "invalid token: #{inspect List.reverse(buffer)}" + end + # Receive an expression content and check # if it is a start or an end token. # Start tokens finish with `do` or `->` diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs index ae1467b2159..85a7a817245 100644 --- a/test/elixir/eex/tokenizer_test.exs +++ b/test/elixir/eex/tokenizer_test.exs @@ -48,4 +48,10 @@ defmodule EEx::TokenizerTest do { :end_expr, '', ' end ' } ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>') end + + test "raise syntax error when there is start mark and no end mark" do + T.tokenize('foo <% :bar', 1) + rescue: error in [EEx::SyntaxError] + assert_equal "invalid token: ' :bar'", error.message + end end diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index 49d8000e7cc..3d215329cba 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -52,6 +52,34 @@ defmodule EExTest do assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1,2,3], \",\" %>" end + test "compile with end of token" do + assert_eval "foo bar %>", "foo bar %>" + end + + test "raises a syntax error when the token is invalid" do + EEx.compile "foo <%= bar" + rescue: error in [EEx::SyntaxError] + assert_equal "invalid token: ' bar'", error.message + end + + test "raises a syntax error when end expression is found without a start expression" do + EEx.compile "foo <% end %>" + rescue: error in [EEx::SyntaxError] + assert_equal "unexpected token: ' end '", error.message + end + + test "raises a syntax error when start expression is found without an end expression" do + EEx.compile "foo <% if true do %>" + rescue: error in [EEx::SyntaxError] + assert_equal "undetermined end of string", error.message + end + + test "raises a syntax error when nested end expression is found without an start expression" do + EEx.compile "foo <%if true do %><% end %><% end %>" + rescue: error in [EEx::SyntaxError] + assert_equal "unexpected token: ' end '", error.message + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From 84849b177a5ff8dc4e2d28a1914d78aac844e697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 28 Feb 2012 18:22:16 -0300 Subject: [PATCH 18/20] Consider lines in embedded string --- lib/eex.ex | 58 ++++++++++++++------------- lib/eex/tokenizer.ex | 48 +++++++++++++---------- test/elixir/eex/tokenizer_test.exs | 63 ++++++++++++++++++++---------- test/elixir/eex_test.exs | 56 ++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 69 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index 5d50be73783..5d77c353477 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -6,62 +6,64 @@ end defexception EEx::SyntaxError, message: nil +defrecord EEx::State, engine: nil, dict: [], filename: nil, line: 0 + defmodule EEx::Compiler do def compile(source, engine) do - tokens = EEx::Tokenizer.tokenize(source) - generate_buffer(tokens, engine, "", [], []) + tokens = EEx::Tokenizer.tokenize(source, 1) + state = EEx::State.new(engine: engine) + generate_buffer(tokens, "", [], state) end - defp generate_buffer([{ :text, chars }|t], engine, buffer, scope, dict) do - buffer = engine.handle_text(buffer, chars) - generate_buffer(t, engine, buffer, scope, dict) + defp generate_buffer([{ :text, _line, chars }|t], buffer, scope, state) do + buffer = state.engine.handle_text(buffer, chars) + generate_buffer(t, buffer, scope, state) end - # TODO: use line and filename - defp generate_buffer([{ :expr, mark, chars }|t], engine, buffer, scope, dict) do - expr = { :__BLOCK__, 0, Erlang.elixir_translator.forms(chars, 1, 'nofile') } - buffer = engine.handle_expr(buffer, mark, expr) - generate_buffer(t, engine, buffer, scope, dict) + # TODO: use filename + defp generate_buffer([{ :expr, line, mark, chars }|t], buffer, scope, state) do + expr = { :__BLOCK__, 0, Erlang.elixir_translator.forms(chars, line, 'nofile') } + buffer = state.engine.handle_expr(buffer, mark, expr) + generate_buffer(t, buffer, scope, state) end - defp generate_buffer([{ :start_expr, _, chars }|t], engine, buffer, scope, _dict) do - { contents, t } = generate_buffer(t, engine, "", [chars|scope], []) - buffer = engine.handle_expr(buffer, '=', contents) - generate_buffer(t, engine, buffer, scope, []) + defp generate_buffer([{ :start_expr, line, _, chars }|t], buffer, scope, state) do + { contents, t } = generate_buffer(t, "", [chars|scope], state.dict([]).line(line)) + buffer = state.engine.handle_expr(buffer, '=', contents) + generate_buffer(t, buffer, scope, state.dict([])) end - defp generate_buffer([{ :middle_expr, _, chars }|t], engine, buffer, [current|scope], dict) do - { wrapped, dict } = wrap_expr(current, buffer, chars, dict) - generate_buffer(t, engine, "", [wrapped|scope], dict) + defp generate_buffer([{ :middle_expr, _, _, chars }|t], buffer, [current|scope], state) do + { wrapped, state } = wrap_expr(current, buffer, chars, state) + generate_buffer(t, "", [wrapped|scope], state) end - defp generate_buffer([{ :end_expr, _, chars }|t], _engine, buffer, [current|_], dict) do - { wrapped, dict } = wrap_expr(current, buffer, chars, dict) - tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(wrapped, 1, 'nofile') } - buffer = insert_quotes(tuples, dict) + defp generate_buffer([{ :end_expr, _, _, chars }|t], buffer, [current|_], state) do + { wrapped, state } = wrap_expr(current, buffer, chars, state) + tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(wrapped, state.line, 'nofile') } + buffer = insert_quotes(tuples, state.dict) { buffer, t } end - defp generate_buffer([{ :end_expr, _, chars }|_], _engine, _buffer, [], _dict) do + defp generate_buffer([{ :end_expr, _, _, chars }|_], _buffer, [], _state) do raise SyntaxError, message: "unexpected token: #{inspect chars}" end - defp generate_buffer([], _engine, buffer, [], _dict) do + defp generate_buffer([], buffer, [], _state) do buffer end - defp generate_buffer([], _engine, _buffer, _scope, _dict) do + defp generate_buffer([], _buffer, _scope, _state) do raise SyntaxError, message: "undetermined end of string" end #### - def wrap_expr(current, buffer, chars, dict) do - key = length(dict) + def wrap_expr(current, buffer, chars, state) do + key = length(state.dict) placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');' - dict = Orddict.put(dict, key, buffer) - { current ++ placeholder ++ chars, dict } + { current ++ placeholder ++ chars, state.merge_dict([{key, buffer}]) } end ### diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index c0ea1756945..88d9955af5f 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -10,17 +10,17 @@ defmodule EEx::Tokenizer do * { :end_expr, marker, contents} """ - def tokenize(bin) when is_binary(bin) do - tokenize(binary_to_list(bin)) + def tokenize(bin, line) when is_binary(bin) do + tokenize(binary_to_list(bin), line) end - def tokenize(list) do - List.reverse(tokenize(list, [], [])) + def tokenize(list, line) do + List.reverse(tokenize(list, line, line, [], [])) end - defp tokenize('<%' ++ t, buffer, acc) do + defp tokenize('<%' ++ t, current_line, line, buffer, acc) do { marker, t } = retrieve_marker(t) - { expr, rest } = tokenize_expr t, [] + { expr, new_line, rest } = tokenize_expr t, line, [] token = tip_expr_token_name(expr) expr = List.reverse(expr) @@ -29,16 +29,20 @@ defmodule EEx::Tokenizer do if token == :expr, do: token = middle_expr_token_name(expr) - acc = tokenize_text(buffer, acc) - tokenize rest, [], [ { token, marker, expr } | acc] + acc = tokenize_text(current_line, buffer, acc) + tokenize rest, new_line, new_line, [], [ { token, line, marker, expr } | acc] end - defp tokenize([h|t], buffer, acc) do - tokenize t, [h|buffer], acc + defp tokenize('\n' ++ t, current_line, line, buffer, acc) do + tokenize t, current_line, line + 1, [?\n|buffer], acc end - defp tokenize([], buffer, acc) do - tokenize_text(buffer, acc) + defp tokenize([h|t], current_line, line, buffer, acc) do + tokenize t, current_line, line, [h|buffer], acc + end + + defp tokenize([], current_line, _line, buffer, acc) do + tokenize_text(current_line, buffer, acc) end # Retrieve marker for <% @@ -53,17 +57,21 @@ defmodule EEx::Tokenizer do # Tokenize an expression until we find %> - defp tokenize_expr('%>' ++ t, buffer) do - { buffer, t } + defp tokenize_expr('%>' ++ t, line, buffer) do + { buffer, line, t } + end + + defp tokenize_expr('\n' ++ t, line, buffer) do + tokenize_expr t, line + 1, [?\n|buffer] end - defp tokenize_expr([h|t], buffer) do - tokenize_expr t, [h|buffer] + defp tokenize_expr([h|t], line, buffer) do + tokenize_expr t, line, [h|buffer] end # Raise an error if the %> is not found - defp tokenize_expr([], buffer) do + defp tokenize_expr([], _line, buffer) do raise EEx::SyntaxError, message: "invalid token: #{inspect List.reverse(buffer)}" end @@ -128,11 +136,11 @@ defmodule EEx::Tokenizer do # Tokenize the buffered text by appending # it to the given accumulator. - defp tokenize_text([], acc) do + defp tokenize_text(_line, [], acc) do acc end - defp tokenize_text(buffer, acc) do - [{ :text, list_to_binary(List.reverse(buffer)) } | acc] + defp tokenize_text(line, buffer, acc) do + [{ :text, line, list_to_binary(List.reverse(buffer)) } | acc] end end diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs index 85a7a817245..62225633a34 100644 --- a/test/elixir/eex/tokenizer_test.exs +++ b/test/elixir/eex/tokenizer_test.exs @@ -5,48 +5,69 @@ defmodule EEx::TokenizerTest do require EEx::Tokenizer, as: T test "simple chars lists" do - assert_equal [ { :text, "foo" } ], T.tokenize('foo') + assert_equal [ { :text, 1, "foo" } ], T.tokenize('foo', 1) end test "simple strings" do - assert_equal [ { :text, "foo" } ], T.tokenize("foo") + assert_equal [ { :text, 1, "foo" } ], T.tokenize("foo", 1) end test "strings with embedded code" do - assert_equal [ { :text, "foo " }, { :expr, [], ' bar ' }], T.tokenize('foo <% bar %>') + assert_equal [ { :text, 1, "foo " }, { :expr, 1, [], ' bar ' } ], T.tokenize('foo <% bar %>', 1) end test "strings with embedded equals code" do - assert_equal [ { :text, "foo " }, { :expr, '=', ' bar ' }], T.tokenize('foo <%= bar %>') + assert_equal [ { :text, 1, "foo " }, { :expr, 1, '=', ' bar ' } ], T.tokenize('foo <%= bar %>', 1) + end + + test "strings with embedded equals code 1" do + assert_equal [ { :text, 1, "foo\n" },{ :expr, 2, '=', ' bar ' } ], T.tokenize('foo\n<%= bar %>', 1) + end + + test "strings with embedded equals code 2" do + string = ''' +foo <%= bar + +baz %> +<% foo %> +''' + + assert_equal [ + {:text, 1, "foo "}, + {:expr, 1, '=', ' bar\n\nbaz '}, + {:text, 3, "\n"}, + {:expr, 4, [], ' foo '}, + {:text, 4, "\n"} + ], T.tokenize(string, 1) end test "strings with embedded do end" do assert_equal [ - { :text, "foo " }, - { :start_expr, '', ' if true do ' }, - { :text, "bar" }, - { :end_expr, '', ' end ' } - ], T.tokenize('foo <% if true do %>bar<% end %>') + { :text, 1, "foo " }, + { :start_expr, 1, '', ' if true do ' }, + { :text, 1, "bar" }, + { :end_expr, 1, '', ' end ' } + ], T.tokenize('foo <% if true do %>bar<% end %>', 1) end test "strings with embedded -> end" do assert_equal [ - { :text, "foo " }, - { :start_expr, '', ' if(true)-> ' }, - { :text, "bar" }, - { :end_expr, '', ' end ' } - ], T.tokenize('foo <% if(true)-> %>bar<% end %>') + { :text, 1, "foo " }, + { :start_expr, 1, '', ' if(true)-> ' }, + { :text, 1, "bar" }, + { :end_expr, 1, '', ' end ' } + ], T.tokenize('foo <% if(true)-> %>bar<% end %>', 1) end test "strings with embedded key-value blocks" do assert_equal [ - { :text, "foo " }, - { :start_expr, '', ' if true do ' }, - { :text, "bar" }, - { :middle_expr, '', ' elsif: false ' }, - { :text, "baz" }, - { :end_expr, '', ' end ' } - ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>') + { :text, 1, "foo " }, + { :start_expr, 1, '', ' if true do ' }, + { :text, 1, "bar" }, + { :middle_expr, 1, '', ' elsif: false ' }, + { :text, 1, "baz" }, + { :end_expr, 1, '', ' end ' } + ], T.tokenize('foo <% if true do %>bar<% elsif: false %>baz<% end %>', 1) end test "raise syntax error when there is start mark and no end mark" do diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index 3d215329cba..45524d36c0c 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -80,6 +80,62 @@ defmodule EExTest do assert_equal "unexpected token: ' end '", error.message end + test "compile respect the lines" do + expected = """ +foo +2 +""" + + string = """ +foo +<%= __LINE__ %> +""" + + assert_eval expected, string + end + + test "compile respect the lines if start expressions" do + expected = """ +foo + +3 + +5 +""" + + string = """ +foo +<% if true do %> +<%= __LINE__ %> +<% end %> +<%= __LINE__ %> +""" + + assert_eval expected, string + end + + test "compile respect the lines if middle expressions" do + expected = """ +foo + +5 + +7 +""" + + string = """ +foo +<% if false do %> +<%= __LINE__ %> +<% else: %> +<%= __LINE__ %> +<% end %> +<%= __LINE__ %> +""" + + assert_eval expected, string + end + defp assert_eval(expected, atual) do compiled = EEx.compile(atual) { result, _ } = Code.eval_quoted(compiled, [], __FILE__, __LINE__) From 8d6dcea7cec5fea49f4a1796e73bf1c4c56032a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 28 Feb 2012 20:01:22 -0300 Subject: [PATCH 19/20] Add more tests to line --- lib/eex.ex | 14 +++++---- lib/eex/tokenizer.ex | 4 +-- test/elixir/eex/tokenizer_test.exs | 6 ++-- test/elixir/eex_test.exs | 48 ++++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index 5d77c353477..c1b5a478a1d 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -33,13 +33,13 @@ defmodule EEx::Compiler do generate_buffer(t, buffer, scope, state.dict([])) end - defp generate_buffer([{ :middle_expr, _, _, chars }|t], buffer, [current|scope], state) do - { wrapped, state } = wrap_expr(current, buffer, chars, state) + defp generate_buffer([{ :middle_expr, line, _, chars }|t], buffer, [current|scope], state) do + { wrapped, state } = wrap_expr(current, line, buffer, chars, state) generate_buffer(t, "", [wrapped|scope], state) end - defp generate_buffer([{ :end_expr, _, _, chars }|t], buffer, [current|_], state) do - { wrapped, state } = wrap_expr(current, buffer, chars, state) + defp generate_buffer([{ :end_expr, line, _, chars }|t], buffer, [current|_], state) do + { wrapped, state } = wrap_expr(current, line, buffer, chars, state) tuples = { :__BLOCK__, 0, Erlang.elixir_translator.forms(wrapped, state.line, 'nofile') } buffer = insert_quotes(tuples, state.dict) { buffer, t } @@ -59,11 +59,13 @@ defmodule EEx::Compiler do #### - def wrap_expr(current, buffer, chars, state) do + def wrap_expr(current, line, buffer, chars, state) do key = length(state.dict) + # TODO: Implement list duplicate + new_lines = :lists.duplicate(line - state.line, ?\n) placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');' - { current ++ placeholder ++ chars, state.merge_dict([{key, buffer}]) } + { current ++ new_lines ++ placeholder ++ chars, state.merge_dict([{key, buffer}]) } end ### diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index 88d9955af5f..1d7746a5aca 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -116,8 +116,8 @@ defmodule EEx::Tokenizer do end defp valid_key_identifier?([h|t]) \ - when h >= ?a and h <= ?z \ - when h >= ?A and h <= ?Z \ + when h >= ?a and h <= ?z \ + when h >= ?A and h <= ?Z \ when h >= ?0 and h <= ?9 do valid_key_identifier?(t) end diff --git a/test/elixir/eex/tokenizer_test.exs b/test/elixir/eex/tokenizer_test.exs index 62225633a34..99c4ea6c819 100644 --- a/test/elixir/eex/tokenizer_test.exs +++ b/test/elixir/eex/tokenizer_test.exs @@ -20,12 +20,12 @@ defmodule EEx::TokenizerTest do assert_equal [ { :text, 1, "foo " }, { :expr, 1, '=', ' bar ' } ], T.tokenize('foo <%= bar %>', 1) end - test "strings with embedded equals code 1" do + test "strings with more than one line" do assert_equal [ { :text, 1, "foo\n" },{ :expr, 2, '=', ' bar ' } ], T.tokenize('foo\n<%= bar %>', 1) end - test "strings with embedded equals code 2" do - string = ''' + test "strings with more than one line and expression with more than one line" do + string = ''' foo <%= bar baz %> diff --git a/test/elixir/eex_test.exs b/test/elixir/eex_test.exs index 45524d36c0c..a99857ec010 100644 --- a/test/elixir/eex_test.exs +++ b/test/elixir/eex_test.exs @@ -80,7 +80,7 @@ defmodule EExTest do assert_equal "unexpected token: ' end '", error.message end - test "compile respect the lines" do + test "respects line numbers" do expected = """ foo 2 @@ -94,7 +94,7 @@ foo assert_eval expected, string end - test "compile respect the lines if start expressions" do + test "respects line numbers inside nested expressions" do expected = """ foo @@ -114,7 +114,49 @@ foo assert_eval expected, string end - test "compile respect the lines if middle expressions" do + test "respects line numbers inside start expression" do + expected = """ +foo + +true + +5 +""" + + string = """ +foo +<% if __LINE__ == 2 do %> +<%= true %> +<% end %> +<%= __LINE__ %> +""" + + assert_eval expected, string + end + + test "respects line numbers inside middle expression" do + expected = """ +foo + +true + +7 +""" + + string = """ +foo +<% if false do %> +<%= false %> +<% elsif: __LINE__ == 4 %> +<%= true %> +<% end %> +<%= __LINE__ %> +""" + + assert_eval expected, string + end + + test "respects line number inside nested expressions with many clauses" do expected = """ foo From e463cfb4865100a810070c2f74a0a72a17dffb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 28 Feb 2012 23:31:45 -0300 Subject: [PATCH 20/20] Change the method visibility and add some docs --- lib/eex.ex | 22 ++++++++++++++-------- lib/eex/tokenizer.ex | 8 ++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/eex.ex b/lib/eex.ex index c1b5a478a1d..8998e49f12d 100644 --- a/lib/eex.ex +++ b/lib/eex.ex @@ -9,12 +9,18 @@ defexception EEx::SyntaxError, message: nil defrecord EEx::State, engine: nil, dict: [], filename: nil, line: 0 defmodule EEx::Compiler do + @moduledoc """ + Get a string source and generate the correspondents quotes + to be evaluated by Elixir. + """ def compile(source, engine) do tokens = EEx::Tokenizer.tokenize(source, 1) state = EEx::State.new(engine: engine) generate_buffer(tokens, "", [], state) end + # Generates the buffers + defp generate_buffer([{ :text, _line, chars }|t], buffer, scope, state) do buffer = state.engine.handle_text(buffer, chars) generate_buffer(t, buffer, scope, state) @@ -57,9 +63,9 @@ defmodule EEx::Compiler do raise SyntaxError, message: "undetermined end of string" end - #### + # Creates a placeholder and wrap it inside the expression block - def wrap_expr(current, line, buffer, chars, state) do + defp wrap_expr(current, line, buffer, chars, state) do key = length(state.dict) # TODO: Implement list duplicate new_lines = :lists.duplicate(line - state.line, ?\n) @@ -68,25 +74,25 @@ defmodule EEx::Compiler do { current ++ new_lines ++ placeholder ++ chars, state.merge_dict([{key, buffer}]) } end - ### + # Changes placeholder to real expression - def insert_quotes( { :__EEX__, _, [key] }, dict) do + defp insert_quotes({ :__EEX__, _, [key] }, dict) do Orddict.get(dict, key) end - def insert_quotes({ left, line, right }, dict) do + defp insert_quotes({ left, line, right }, dict) do { insert_quotes(left, dict), line, insert_quotes(right, dict) } end - def insert_quotes({ left, right }, dict) do + defp insert_quotes({ left, right }, dict) do { insert_quotes(left, dict), insert_quotes(right, dict) } end - def insert_quotes(list, dict) when is_list(list) do + defp insert_quotes(list, dict) when is_list(list) do Enum.map list, insert_quotes(&1, dict) end - def insert_quotes(other, _dict) do + defp insert_quotes(other, _dict) do other end end diff --git a/lib/eex/tokenizer.ex b/lib/eex/tokenizer.ex index 1d7746a5aca..b85f91ff30c 100644 --- a/lib/eex/tokenizer.ex +++ b/lib/eex/tokenizer.ex @@ -4,10 +4,10 @@ defmodule EEx::Tokenizer do @doc """ Tokenizes the given char list. It returns 4 tokens as result: - * { :text, contents } - * { :expr, marker, contents} - * { :start_expr, marker, contents} - * { :end_expr, marker, contents} + * { :text, line, contents } + * { :expr, line, marker, contents} + * { :start_expr, line, marker, contents} + * { :end_expr, line, marker, contents} """ def tokenize(bin, line) when is_binary(bin) do