defmodule Surface.TypeHandler do @moduledoc false alias Surface.IOHelper alias Surface.Components.Dynamic @type clauses :: list(Macro.t()) @type opts :: keyword(Macro.t()) @callback literal_to_ast_node( type :: atom(), name :: atom(), value :: any(), meta :: Surface.AST.Meta.t() ) :: {:ok, Surface.AST.Literal.t() | Surface.AST.AttributeExpr.t()} | {:error, String.t()} | :error @callback expr_to_quoted( type :: atom(), name :: atom(), clauses(), opts(), module(), original :: String.t() ) :: {:ok, Macro.t()} | {:error, String.t()} | :error @callback expr_to_value(clauses :: list(), opts :: keyword(), ctx :: map()) :: {:ok, any()} | {:error, any()} | {:error, any(), String.t()} @callback value_to_html(name :: atom(), value :: any()) :: {:ok, String.t() | nil | boolean()} | {:error, String.t()} @callback value_to_opts(name :: atom(), value :: any()) :: {:ok, any()} | {:error, String.t()} @optional_callbacks [ literal_to_ast_node: 4, expr_to_quoted: 6, expr_to_value: 3, value_to_html: 2, value_to_opts: 2 ] @boolean_tag_attributes [ :allowfullscreen, :allowpaymentrequest, :async, :autofocus, :autoplay, :checked, :controls, :default, :defer, :disabled, :formnovalidate, :hidden, :ismap, :itemscope, :loop, :multiple, :muted, :nomodule, :novalidate, :open, :readonly, :required, :reversed, :selected, :typemustmatch, :"phx-page-loading" ] @phx_event_attributes Surface.Directive.Events.phx_events() |> Enum.map(&String.to_atom/1) defmacro __using__(_) do quote do @behaviour unquote(__MODULE__) @default_handler unquote(__MODULE__).Default if __MODULE__ != @default_handler do @impl true defdelegate literal_to_ast_node(type, name, value, meta), to: @default_handler @impl true defdelegate expr_to_quoted(type, name, clauses, opts, module, original), to: @default_handler @impl true defdelegate expr_to_value(clauses, opts, ctx), to: @default_handler @impl true defdelegate value_to_html(name, value), to: @default_handler @impl true defdelegate value_to_opts(name, value), to: @default_handler defoverridable literal_to_ast_node: 4, expr_to_quoted: 6, expr_to_value: 3, value_to_html: 2, value_to_opts: 2 end end end def literal_to_ast_node!(type, name, value, meta) do case handler(type).literal_to_ast_node(type, name, value, meta) do {:ok, attr_value} -> attr_value {:error, expected} -> message = compile_error_message(type, name, as_literal(value), meta.module, expected) IOHelper.compile_error(message, meta.file, meta.line) _ -> message = compile_error_message(type, name, as_literal(value), meta.module) IOHelper.compile_error(message, meta.file, meta.line) end end def expr_to_quoted!(value, name, type, meta, original \\ nil) do original = original || value with {:ok, ast} <- normalize_expr(value, line: meta.line, file: meta.file), _ <- Surface.Compiler.Helpers.perform_assigns_checks(ast, meta), {clauses, opts} <- split_clauses_and_options(ast), true <- clauses != [] or opts != [], handler <- handler(type), {:ok, value} <- handler.expr_to_quoted(type, name, clauses, opts, meta, original) do value else {:error, {position, error, token}} -> IOHelper.syntax_error( error <> token, meta.file, Surface.Compiler.Helpers.position_to_line(position) ) {:error, expected} -> message = compile_error_message(type, name, as_expr(original), meta.module, expected) IOHelper.compile_error(message, meta.file, meta.line) _ -> message = compile_error_message(type, name, as_expr(original), meta.module) IOHelper.compile_error(message, meta.file, meta.line) end end def expr_to_value!(type, name, clauses, opts, module, original, ctx) do case handler(type).expr_to_value(clauses, opts, ctx) do {:ok, value} -> value {:error, value} -> message = runtime_error_message(type, name, value, module, original) IOHelper.runtime_error(message) {:error, value, details} -> message = runtime_error_message(type, name, value, module, original, details) IOHelper.runtime_error(message) end end def attr_to_html!(type, name, value) do case attr_to_html(type, name, value) do {:ok, value} -> value {:error, message} -> IOHelper.runtime_error(message) end end def attr_to_html(type, name, value) do case handler(type).value_to_html(name, value) do {:ok, val} when val in ["", nil, false] -> {:ok, ""} {:ok, true} -> {:ok, [~S( ), to_string(name)]} {:ok, val} -> attr_value = [{name, val}] |> Phoenix.HTML.attributes_escape() |> Phoenix.HTML.safe_to_string() {:ok, attr_value} {:error, message} -> {:error, message} end end def attr_to_opts!(type, name, value) do case handler(type).value_to_opts(name, value) do {:ok, val} when val in ["", nil, false] -> [] {:ok, val} -> [{name, val}] {:ok, :value, ""} -> [{:value, ""}] {:error, message} -> IOHelper.runtime_error(message) end end def runtime_prop_value!(module, name, clauses, opts, node_alias, original, ctx) do caller = %Macro.Env{module: ctx.module} {type, type_opts} = attribute_type_and_opts(module, name, %{ runtime: true, node_alias: node_alias || module, caller: caller, file: ctx.file, line: ctx.line }) if Keyword.get(type_opts, :accumulate, false) do Enum.map(clauses, &expr_to_value!(type, name, [&1], opts, module, original, ctx)) else if length(clauses) > 1 do message = if Keyword.get(type_opts, :root, false) do """ the prop `#{name}` has been passed multiple times. Considering only the last value. Hint: Either specify the `#{name}` via the root property (`<#{node_alias} { ... }>`) or \ explicitly via the #{name} property (`<#{node_alias} #{name}="...">`), but not both. """ else """ the prop `#{name}` has been passed multiple times. Considering only the last value. Hint: Either remove all redundant definitions or set option `accumulate` to `true`: ``` prop #{name}, #{inspect(type)}, accumulate: true ``` This way the values will be accumulated in a list. """ end IOHelper.warn(message, caller, ctx.file, ctx.line) end expr_to_value!(type, name, [List.last(clauses)], opts, module, original, ctx) end end def attribute_type_and_opts(name) do attribute_type_and_opts(nil, name, nil) end def attribute_type_and_opts(nil, :class, _meta), do: {:css_class, []} def attribute_type_and_opts(nil, :style, _meta), do: {:style, []} def attribute_type_and_opts(nil, name, _meta) when name in @boolean_tag_attributes, do: {:boolean, []} def attribute_type_and_opts(nil, name, _meta) when name in @phx_event_attributes, do: {:phx_event, []} def attribute_type_and_opts(nil, _name, _meta), do: {:string, []} # TODO: If we add a property to define the list of available modules or create # the concept of interfaces, we could validate/retrieve the types and opts. def attribute_type_and_opts(Dynamic.Component, :module, _meta) do {:module, []} end def attribute_type_and_opts(Dynamic.Component, :function, _meta) do {:atom, []} end def attribute_type_and_opts(Dynamic.Component, _name, _meta) do {:dynamic, []} end def attribute_type_and_opts(Dynamic.LiveComponent, :module, _meta) do {:module, []} end def attribute_type_and_opts(Dynamic.LiveComponent, _name, _meta) do {:dynamic, []} end def attribute_type_and_opts(_, :__caller_scope_id__, _meta) do {:string, []} end def attribute_type_and_opts(module, name, _meta) do with true <- function_exported?(module, :__get_prop__, 1), prop when not is_nil(prop) <- module.__get_prop__(name) do {prop.type, prop.opts} else _ -> {:string, []} end end defp runtime_error_message(type, name, value, module, original, details \\ nil) do name = name || Enum.find(module.__props__(), & &1.opts[:root]).name details = if details, do: "\n" <> details, else: details {attr_name, attr_kind} = formatted_name_and_kind(name, module) original_expr_msg = if original, do: "\nOriginal expression: {#{original}}", else: "" """ invalid value for #{attr_kind} #{attr_name}. \ Expected a #{inspect(type)}, got: #{inspect(value)}. #{original_expr_msg} #{details}\ """ end defp compile_error_message(type, name, value, module, expected \\ nil) do expected = expected || "Expected a #{inspect(type)}" {attr_name, attr_kind} = formatted_name_and_kind(name, module) """ invalid value for #{attr_kind} #{attr_name}. \ #{expected}, got: #{value}.\ """ end defp formatted_name_and_kind(name, module) do cond do to_string(name) |> String.starts_with?(":") -> {name, "directive"} module -> name = name || Enum.find(module.__props__(), & &1.opts[:root]).name {inspect("#{name}"), "property"} true -> {inspect("#{name}"), "attribute"} end end defp normalize_expr(expr, opts) when is_binary(expr) do with {:ok, {:wrap, _, ast}} <- Code.string_to_quoted("wrap(#{expr})", opts) do {:ok, ast} end end defp normalize_expr(expr, _opts) do {:ok, [expr]} end defp split_clauses_and_options(clauses_and_options) do with [[_ | _] = opts | clauses] <- Enum.reverse(clauses_and_options), true <- Keyword.keyword?(opts) do {Enum.reverse(clauses), opts} else _ -> {clauses_and_options, []} end end defp as_literal(value) do inspect(value) end defp as_expr(value) do "{#{value}}" end defp handler(:boolean), do: __MODULE__.Boolean defp handler(:atom), do: __MODULE__.Atom defp handler(:form), do: __MODULE__.Form defp handler(:map), do: __MODULE__.Map defp handler(:keyword), do: __MODULE__.Keyword defp handler(:css_class), do: __MODULE__.CssClass defp handler(:style), do: __MODULE__.Style defp handler(:event), do: __MODULE__.Event defp handler(:phx_event), do: __MODULE__.PhxEvent defp handler(:generator), do: __MODULE__.Generator defp handler(:list), do: __MODULE__.List defp handler(:static_list), do: __MODULE__.StaticList defp handler(:context_put), do: __MODULE__.ContextPut defp handler(:context_get), do: __MODULE__.ContextGet defp handler(:hook), do: __MODULE__.Hook defp handler(:dynamic), do: __MODULE__.Dynamic defp handler(:let_arg), do: __MODULE__.LetArg defp handler(:render_slot), do: __MODULE__.RenderSlot defp handler(_), do: __MODULE__.Default end