Skip to content

Commit 039ca64

Browse files
author
José Valim
committed
Add handle_begin and handle_end to EEx
Elixir v1.5.0 attempted to repurpose the value of init to use throughout the engine but such was a breaking change. To address this problem, we added handle_begin and handle_end which were designed exactly to handle inner buffers. This comes with the benefit that EEx.Engine can now have any data structure as state and no longer only quoted expressions. Closes #6391.
1 parent 705952b commit 039ca64

File tree

3 files changed

+70
-24
lines changed

3 files changed

+70
-24
lines changed

lib/eex/lib/eex/compiler.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ defmodule EEx.Compiler do
1616
trim = opts[:trim] || false
1717
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
1818
{:ok, tokens} ->
19-
state = %{engine: opts[:engine] || @default_engine, init: nil,
19+
state = %{engine: opts[:engine] || @default_engine,
2020
file: file, line: line, quoted: [], start_line: nil}
2121
init = state.engine.init(opts)
22-
generate_buffer(tokens, init, [], %{state | init: init})
22+
generate_buffer(tokens, init, [], state)
2323
{:error, line, message} ->
2424
raise EEx.SyntaxError, line: line, file: file, message: message
2525
end
@@ -42,15 +42,15 @@ defmodule EEx.Compiler do
4242
defp generate_buffer([{:start_expr, start_line, mark, chars} | rest], buffer, scope, state) do
4343
{contents, line, rest} = look_ahead_text(rest, start_line, chars)
4444
{contents, rest} =
45-
generate_buffer(rest, state.init, [contents | scope],
45+
generate_buffer(rest, state.engine.handle_begin(buffer), [contents | scope],
4646
%{state | quoted: [], line: line, start_line: start_line})
4747
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
4848
generate_buffer(rest, buffer, scope, state)
4949
end
5050

5151
defp generate_buffer([{:middle_expr, line, '', chars} | rest], buffer, [current | scope], state) do
5252
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
53-
generate_buffer(rest, state.init, [wrapped | scope], %{state | line: line})
53+
generate_buffer(rest, state.engine.handle_begin(buffer), [wrapped | scope], %{state | line: line})
5454
end
5555

5656
defp generate_buffer([{:middle_expr, line, modifier, chars} | t], buffer, scope, state) do
@@ -99,7 +99,7 @@ defmodule EEx.Compiler do
9999
key = length(state.quoted)
100100
placeholder = '__EEX__(' ++ Integer.to_charlist(key) ++ ');'
101101
{current ++ placeholder ++ new_lines ++ chars,
102-
%{state | quoted: [{key, buffer} | state.quoted]}}
102+
%{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}}
103103
end
104104

105105
# Look text ahead on expressions

lib/eex/lib/eex/engine.ex

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@ defmodule EEx.Engine do
22
@moduledoc ~S"""
33
Basic EEx engine that ships with Elixir.
44
5-
An engine needs to implement four functions:
5+
An engine needs to implement six functions:
66
7-
* `init(opts)` - returns the initial buffer
7+
* `init(opts)` - called at the beginning of every text
8+
and it must return the initial state.
89
9-
* `handle_body(quoted)` - receives the final built quoted
10-
expression, should do final post-processing and return a
11-
quoted expression.
10+
* `handle_body(state)` - receives the state of the document
11+
and it must return a quoted expression.
1212
13-
* `handle_text(buffer, text)` - it receives the buffer,
13+
* `handle_text(state, text)` - it receives the state,
1414
the text and must return a new quoted expression.
1515
16-
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
17-
the marker, the expr and must return a new quoted expression.
16+
* `handle_expr(state, marker, expr)` - it receives the state,
17+
the marker, the expr and must return a new state.
18+
19+
* `handle_begin(state)` - called every time there a new state
20+
is needed with an empty buffer. Typically called for do/end
21+
blocks, case expressions, anonymous functions, etc
22+
23+
* `handle_end(state)` - opposite of `handle_begin(state)` and
24+
it must return quoted expression
1825
1926
The marker is what follows exactly after `<%`. For example,
2027
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
@@ -27,10 +34,14 @@ defmodule EEx.Engine do
2734
default implementations for the functions above.
2835
"""
2936

30-
@callback init(opts :: keyword) :: Macro.t
31-
@callback handle_body(quoted :: Macro.t) :: Macro.t
32-
@callback handle_text(buffer :: Macro.t, text :: String.t) :: Macro.t
33-
@callback handle_expr(buffer :: Macro.t, marker :: String.t, expr :: Macro.t) :: Macro.t
37+
@type state :: term
38+
39+
@callback init(opts :: keyword) :: state
40+
@callback handle_body(state) :: Macro.t
41+
@callback handle_text(state, text :: String.t) :: state
42+
@callback handle_expr(state, marker :: String.t, expr :: Macro.t) :: state
43+
@callback handle_begin(state) :: state
44+
@callback handle_end(state) :: Macro.t
3445

3546
@doc false
3647
defmacro __using__(_) do
@@ -45,6 +56,14 @@ defmodule EEx.Engine do
4556
EEx.Engine.handle_body(quoted)
4657
end
4758

59+
def handle_begin(quoted) do
60+
EEx.Engine.handle_begin(quoted)
61+
end
62+
63+
def handle_end(quoted) do
64+
EEx.Engine.handle_end(quoted)
65+
end
66+
4867
def handle_text(buffer, text) do
4968
EEx.Engine.handle_text(buffer, text)
5069
end
@@ -104,6 +123,20 @@ defmodule EEx.Engine do
104123
""
105124
end
106125

126+
@doc """
127+
Returns an empty string as the new buffer.
128+
"""
129+
def handle_begin(_previous) do
130+
""
131+
end
132+
133+
@doc """
134+
End of the new buffer.
135+
"""
136+
def handle_end(quoted) do
137+
quoted
138+
end
139+
107140
@doc """
108141
The default implementation simply returns the given expression.
109142
"""

lib/eex/test/eex_test.exs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,25 +436,38 @@ defmodule EExTest do
436436
@behaviour EEx.Engine
437437

438438
def init(_opts) do
439-
""
439+
"INIT"
440440
end
441441

442442
def handle_body(body) do
443-
{:wrapped, body}
443+
"BODY(#{body})"
444+
end
445+
446+
def handle_begin(_) do
447+
"BEGIN"
448+
end
449+
450+
def handle_end(buffer) do
451+
buffer <> ":END"
444452
end
445453

446454
def handle_text(buffer, text) do
447-
EEx.Engine.handle_text(buffer, text)
455+
buffer <> ":TEXT(#{String.trim(text)})"
448456
end
449457

450-
def handle_expr(buffer, mark, expr) do
451-
EEx.Engine.handle_expr(buffer, mark, expr)
458+
def handle_expr(buffer, "=", expr) do
459+
buffer <> ":EQUAL(#{Macro.to_string(expr)})"
452460
end
453461
end
454462

455463
describe "custom engines" do
456-
test "calls handle_body" do
457-
assert {:wrapped, "foo"} = EEx.eval_string("foo", [], engine: TestEngine)
464+
test "text" do
465+
assert_eval "BODY(INIT:TEXT(foo))", "foo", [], engine: TestEngine
466+
end
467+
468+
test "begin/end" do
469+
assert_eval ~s[BODY(INIT:TEXT(foo):EQUAL(if() do\n "BEGIN:TEXT(this):END"\nelse\n "BEGIN:TEXT(that):END"\nend))],
470+
"foo <%= if do %>this<% else %>that<% end %>", [], engine: TestEngine
458471
end
459472
end
460473

0 commit comments

Comments
 (0)