1defmodule EEx.Engine do
2  @moduledoc ~S"""
3  Basic EEx engine that ships with Elixir.
4
5  An engine needs to implement all callbacks below.
6
7  This module also ships with a default engine implementation
8  you can delegate to. See `EEx.SmartEngine` as an example.
9  """
10
11  @type state :: term
12
13  @doc """
14  Called at the beginning of every template.
15
16  It must return the initial state.
17  """
18  @callback init(opts :: keyword) :: state
19
20  @doc """
21  Called at the end of every template.
22
23  It must return Elixir's quoted expressions for the template.
24  """
25  @callback handle_body(state) :: Macro.t()
26
27  @doc """
28  Called for the text/static parts of a template.
29
30  It must return the updated state.
31  """
32  @callback handle_text(state, [line: pos_integer, column: pos_integer], text :: String.t()) ::
33              state
34
35  @doc """
36  Called for the dynamic/code parts of a template.
37
38  The marker is what follows exactly after `<%`. For example,
39  `<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
40  as marker. The allowed markers so far are:
41
42    * `""`
43    * `"="`
44    * `"/"`
45    * `"|"`
46
47  Markers `"/"` and `"|"` are only for use in custom EEx engines
48  and are not implemented by default. Using them without an
49  appropriate implementation raises `EEx.SyntaxError`.
50
51  It must return the updated state.
52  """
53  @callback handle_expr(state, marker :: String.t(), expr :: Macro.t()) :: state
54
55  @doc """
56  Invoked at the beginning of every nesting.
57
58  It must return a new state that is used only inside the nesting.
59  Once the nesting terminates, the current `state` is resumed.
60  """
61  @callback handle_begin(state) :: state
62
63  @doc """
64  Invokes at the end of a nesting.
65
66  It must return Elixir's quoted expressions for the nesting.
67  """
68  @callback handle_end(state) :: Macro.t()
69
70  @doc false
71  @deprecated "Use explicit delegation to EEx.Engine instead"
72  defmacro __using__(_) do
73    quote do
74      @behaviour EEx.Engine
75
76      def init(opts) do
77        EEx.Engine.init(opts)
78      end
79
80      def handle_body(state) do
81        EEx.Engine.handle_body(state)
82      end
83
84      def handle_begin(state) do
85        EEx.Engine.handle_begin(state)
86      end
87
88      def handle_end(state) do
89        EEx.Engine.handle_end(state)
90      end
91
92      def handle_text(state, text) do
93        EEx.Engine.handle_text(state, [], text)
94      end
95
96      def handle_expr(state, marker, expr) do
97        EEx.Engine.handle_expr(state, marker, expr)
98      end
99
100      defoverridable EEx.Engine
101    end
102  end
103
104  @doc """
105  Handles assigns in quoted expressions.
106
107  A warning will be printed on missing assigns.
108  Future versions will raise.
109
110  This can be added to any custom engine by invoking
111  `handle_assign/1` with `Macro.prewalk/2`:
112
113      def handle_expr(state, token, expr) do
114        expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
115        super(state, token, expr)
116      end
117
118  """
119  @spec handle_assign(Macro.t()) :: Macro.t()
120  def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
121    line = meta[:line] || 0
122    quote(line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name)))
123  end
124
125  def handle_assign(arg) do
126    arg
127  end
128
129  @doc false
130  # TODO: Raise on v2.0
131  @spec fetch_assign!(Access.t(), Access.key()) :: term | nil
132  def fetch_assign!(assigns, key) do
133    case Access.fetch(assigns, key) do
134      {:ok, val} ->
135        val
136
137      :error ->
138        keys = Enum.map(assigns, &elem(&1, 0))
139
140        IO.warn(
141          "assign @#{key} not available in EEx template. " <>
142            "Please ensure all assigns are given as options. " <>
143            "Available assigns: #{inspect(keys)}"
144        )
145
146        nil
147    end
148  end
149
150  @doc "Default implementation for `c:init/1`."
151  def init(_opts) do
152    %{
153      binary: [],
154      dynamic: [],
155      vars_count: 0
156    }
157  end
158
159  @doc "Default implementation for `c:handle_begin/1`."
160  def handle_begin(state) do
161    check_state!(state)
162    %{state | binary: [], dynamic: []}
163  end
164
165  @doc "Default implementation for `c:handle_end/1`."
166  def handle_end(quoted) do
167    handle_body(quoted)
168  end
169
170  @doc "Default implementation for `c:handle_body/1`."
171  def handle_body(state) do
172    check_state!(state)
173    %{binary: binary, dynamic: dynamic} = state
174    binary = {:<<>>, [], Enum.reverse(binary)}
175    dynamic = [binary | dynamic]
176    {:__block__, [], Enum.reverse(dynamic)}
177  end
178
179  @doc "Default implementation for `c:handle_text/3`."
180  def handle_text(state, _meta, text) do
181    check_state!(state)
182    %{binary: binary} = state
183    %{state | binary: [text | binary]}
184  end
185
186  @doc "Default implementation for `c:handle_expr/3`."
187  def handle_expr(state, "=", ast) do
188    check_state!(state)
189    %{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
190    var = Macro.var(:"arg#{vars_count}", __MODULE__)
191
192    ast =
193      quote do
194        unquote(var) = String.Chars.to_string(unquote(ast))
195      end
196
197    segment =
198      quote do
199        unquote(var) :: binary
200      end
201
202    %{state | dynamic: [ast | dynamic], binary: [segment | binary], vars_count: vars_count + 1}
203  end
204
205  def handle_expr(state, "", ast) do
206    %{dynamic: dynamic} = state
207    %{state | dynamic: [ast | dynamic]}
208  end
209
210  def handle_expr(_state, marker, _ast) when marker in ["/", "|"] do
211    raise EEx.SyntaxError,
212          "unsupported EEx syntax <%#{marker} %> (the syntax is valid but not supported by the current EEx engine)"
213  end
214
215  defp check_state!(%{binary: _, dynamic: _, vars_count: _}), do: :ok
216
217  defp check_state!(state) do
218    raise "unexpected EEx.Engine state: #{inspect(state)}. " <>
219            "This typically means a bug or an outdated EEx.Engine or tool"
220  end
221end
222