defmodule Mix.Tasks.Moon.Light.Component do @shortdoc "Converts surface component to phoenix_live_view" @moduledoc """ Generates a component and a compoinent page by component module name given $ mix moon.light.component Form.MyModule """ use Mix.Task import Mix.Generator import Moon.Helpers.Ast import String, only: [replace: 3, downcase: 1, split: 2] import List, only: [last: 1] import Moon.Helpers.Tokens require Logger alias Surface.Compiler.Parser defstruct name: nil, path: nil @doc false def run([name]) do name |> context() |> add_module_info() |> copy_templates() |> print_shell_instructions() end defp context(name) do %{ name: name, module: String.to_existing_atom("Elixir.Moon.Design.#{name}"), path: name |> replace(~r/([a-z])([A-Z])/, "\\1_\\2") |> downcase() |> replace(".", "/"), short: name |> split(".") |> last(), config: %{ use_translates: fn alias -> %{ [:Moon, :StatelessComponent] => [:Moon, :Light, :Component], [:Moon, :StatefulComponent] => [:Moon, :Light, :LiveComponent] }[alias] end, defmodule_translates: fn [:Moon | [:Design | others]] -> [:Moon | [:Light | others]] end, module_translates: fn module -> module |> to_string() |> String.replace("Moon.Design", "Moon.Light") |> String.to_atom() end } } end defp add_module_info(context = %{module: module}) do Map.merge( %{ component_type: module.component_type(), level: 0 }, context ) end defp translate_ast(c = %{module: module}) do ast = module |> module_ast() {_, aliases} = ast |> Macro.prewalk([], fn result = {:alias, _, [{:__aliases__, _, a} | _]}, acc -> {result, acc ++ [a]} result = {:alias, _, [{{:., _, [{:__aliases__, _, root_part}, :{}]}, _, children}]}, acc -> {result, acc ++ (children |> Enum.map(fn {:__aliases__, _, sub_part} -> root_part ++ sub_part end))} other, acc -> {other, acc} end) c = Map.put( c, :aliases, aliases |> Enum.reduce(%{}, fn items, acc -> Map.put( acc, Enum.at(items, -1) |> to_string(), [:"Elixir" | items] |> Enum.map_join(".", &to_string/1) |> String.to_atom() ) end) ) ast |> Macro.prewalk(nil, fn result = {:def, _, [{:render, _, _} | _]}, acc -> {translate_render(c, result), acc} result = {:sigil_F, _, _}, acc -> {translate_sigil(c, result), acc} result = {:prop, _meta, _options}, acc -> {translate_prop(c, result, acc), nil} result = {:slot, _meta, _options}, acc -> {translate_slot(c, result, acc), nil} result = {:@, _, [{:moduledoc, _, [_text]}]}, acc -> {translate_moduledoc(c, result), acc} result = {:@, _, [{:doc, _, [_text]}]}, _acc -> {:skip, translate_doc(c, result)} result = {:use, _, [{:__aliases__, _, _alias} | _]}, acc -> {translate_use_defmodule(c, result), acc} result = {:defmodule, _, [{:__aliases__, _, _alias} | _]}, acc -> {translate_use_defmodule(c, result), acc} other, acc -> {other, acc} end) |> Tuple.to_list() |> hd() end defp merge_ast( {:defmodule, m1, [ aliases, [ do: {:__block__, [], children} ] ]}, {:defmodule, _, [ _aliases, [ do: {:__block__, [], children2} ] ]} ) do {:defmodule, m1, [ aliases, [ do: {:__block__, [], (children ++ children2) |> Enum.sort_by(fn {:@, _, [{:moduledoc, _, _}]} -> 0 {:use, _, _} -> 1 {:alias, _, _} -> 2 {:import, _, _} -> 3 {:require, _, _} -> 4 _ -> 100 end)} ] ]} end defp translate_render(%{component_type: Surface.LiveComponent}, ast), do: ast defp translate_render( %{component_type: Surface.Component, short: short}, {:def, m1, [{:render, m2, attrs} | children]} ) do {:def, m1, [{:"#{String.downcase(short)}", m2, attrs} | children]} end # translate {:prop, [line: 21], [{:step, [line: 21], nil}, :integer, [default: 1]]} # to {:attr, [line: 38], [:id, :string, [required: true]]} defp translate_prop(_, {:prop, meta, [{name, _, nil}, type]}, doc), do: {:attr, meta, [name, translate_prop_type(type), (doc && [doc: doc]) || []]} defp translate_prop(_, {:prop, meta, [{name, _, nil}, type, options]}, doc), do: {:attr, meta, [ name, translate_prop_type(type), translate_prop_options(options) ++ ((doc && [doc: doc]) || []) ]} defp translate_prop_type(type) when type in [:boolean, :integer, :float, :string, :atom, :list, :map, :global], do: type defp translate_prop_type(_), do: :any defp translate_prop_options(options) do options |> Enum.map(fn {:values!, v} -> {:values, v} {k, v} when k in [:required, :default, :values, :examples] -> {k, v} end) end defp translate_slot(_, {:slot, meta, [{:default, _, nil}]}, doc), do: {:slot, meta, [:inner_block, (doc && [doc: doc]) || []]} defp translate_slot(_, {:slot, meta, [{name, _, nil}]}, doc), do: {:slot, meta, [name, (doc && [doc: doc]) || []]} defp translate_slot(_, {:slot, meta, [{:default, _, nil}, options]}, doc), do: {:slot, meta, [:inner_block, options ++ ((doc && [doc: doc]) || [])]} defp translate_slot(_, {:slot, meta, [{name, _, nil}, options]}, doc), do: {:slot, meta, [name, options ++ ((doc && [doc: doc]) || [])]} defp translate_doc(_, _res = {:@, _, [{:doc, _, [text]}]}) do text end defp translate_moduledoc(%{component_type: Surface.LiveComponent}, res), do: res defp translate_moduledoc( %{component_type: Surface.Component}, {:@, m1, [{:moduledoc, m2, [text]}]} ), do: (text && {:@, m1, [{:doc, m2, [dbg(text)]}]}) || :skip defp translate_use_defmodule( %{component_type: Surface.Component}, {:use, _, _} ) do :skip end defp translate_use_defmodule( %{config: config}, {type, m1, [{:__aliases__, m2, alias} | other]} ) when type in [:use, :defmodule] do {type, m1, [{:__aliases__, m2, config[:"#{type}_translates"].(alias)} | other]} end defp translate_sigil(context, {:sigil_F, m1, [{:<<>>, meta, [string]}, opts]}) do # "copy of Surface.sigil_F macro & Surface.Compiler.compile" result = string |> Parser.parse!( # file: module.__info__(:compile)[:source] |> to_string(), # line: meta[:line] + if(Keyword.has_key?(meta, :indentation), do: 1, else: 0), # caller: %{module: module, function: :render}, # checks: opts[:checks] || [], # warnings: opts[:warnings] || [], # column: Keyword.get(opts, :column, 1), # indentation: Keyword.get(opts, :indentation, 0) ) |> prewalk(context, fn # :text, text, context -> # {text, context} _node_type, node, context -> {translate_node(node, context), context} end) |> Tuple.to_list() |> hd() |> to_text() {:sigil_H, m1, [{:<<>>, meta, [result]}, opts]} end defp translate_node(text, _) when is_binary(text), do: text defp translate_node({:expr, expr, meta}, _), do: {:expr, expr, meta} # TODO: context_put & children defp translate_node( {"#slot", [{:root, {:attribute_expr, expr, _m2}, _m1} | _], _children, node_meta}, _ ) do {:expr, "render_slot(#{expr})", node_meta} end defp translate_node({"#slot", _, _children, node_meta}, _) do {:expr, "render_slot(@inner_block)", node_meta} end defp translate_node({type, attributes, children, node_meta}, c) do {type |> translate_type(c), attributes |> List.wrap() |> Enum.map(&translate_attr/1), children, node_meta} end defp translate_node(other, _), do: other defp translate_type(type, c = %{aliases: aliases}) do cond do !String.match?(type, ~r/^[A-Z].*$/) -> type type == "Icon" -> ".icon" aliases[type] === nil -> Logger.warning("Unknown type: ", type) dbg(type) aliases[type].component_type() == Surface.Component -> mod = aliases[type] |> parent_module() |> c.config.module_translates.() if function_exported?(mod, type |> String.downcase() |> String.to_atom(), 1) do "#{(mod == c.config.module_translates.(c.module) && "") || mod}.#{type |> String.downcase()}" else ".moon module={#{type}}" end aliases[type].component_type() == Surface.LiveComponent -> mod = aliases[type] |> c.config.module_translates.() if function_exported?(mod, :render, 1) do ".live_component module={#{mod}}" else ".moon module={#{type}}" end end end # defp translate_attr({name, expr, meta}), do: dbg({name, expr, meta}) defp translate_attr({name, expr, meta}) when is_atom(name), do: translate_attr({"#{name}", expr, meta}) defp translate_attr({name, expr, meta}) when name in ~w(root :let :if :for), do: {name, expr, meta} # TODO: translate each to data-value attribute defp translate_attr({":values", value, meta}) do Logger.warning(":value attr shoud be expanded: #{inspect(value)}") {"values", translate_attr_value(value), meta} end defp translate_attr({":" <> name, value, meta}), do: {:"phx-#{name}", translate_attr_value(value), meta} defp translate_attr({name, value, meta}), do: {:"#{name}", translate_attr_value(value), meta} defp translate_attr_value({:attribute_expr, expr, meta}) do case Code.string_to_quoted(expr) do {:ok, _} -> {:attribute_expr, expr, meta} {:error, _} -> {:attribute_expr, "[#{expr}]", meta} end end defp translate_attr_value(expr), do: expr defp copy_templates(context = %{component_type: Surface.LiveComponent}) do context.module |> context.config.module_translates.() |> module_to_path() |> create_file(translate_ast(context) |> Macro.to_string() |> String.replace(" :skip\n", "")) context end defp copy_templates(context = %{component_type: Surface.Component}) do target_module = context.module |> parent_module() |> context.config.module_translates.() target_module |> module_to_path() |> create_file( case target_module |> Code.ensure_compiled() do {:module, _} -> target_module |> module_ast() {:error, :nofile} -> "priv/templates/moon.light.component/empty_module.ex" |> EEx.eval_file(name: target_module) |> Code.string_to_quoted!() end |> merge_ast(translate_ast(context)) |> Macro.to_string() |> String.replace(" :skip\n", "") ) context end defp module_to_path(module) do (module |> to_string() |> replace(~r/([a-z])([A-Z])/, "\\1_\\2") |> downcase() |> replace(".", "/") |> replace("elixir/", "lib/")) <> ".exs" end defp parent_module(module) do module |> to_string() |> String.replace(~r/\.[A-Za-z]*$/, "") |> String.to_atom() end def print_shell_instructions(%{path: _path, name: _name}) do Mix.shell().info(""" """) end end