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