Packages
moon
2.78.3
2.90.5
retired
2.90.4
2.90.3
2.90.2
2.90.1
2.90.0
2.89.4
2.89.1
2.89.0
2.88.0
2.87.11
2.87.10
2.87.9
2.87.8
2.87.7
2.87.6
2.87.5
2.87.4
2.87.3
2.87.2
2.87.1
2.87.0
2.86.1
2.86.0
2.85.1
2.85.0
2.84.0
2.83.0
2.82.0
2.81.4
2.81.3
2.81.2
2.81.1
2.81.0
2.80.2
2.80.1
2.80.0
2.79.13
2.79.12
2.79.11
2.79.10
2.79.9
2.79.8
2.79.7
2.79.6
2.79.5
2.79.4
2.79.3
2.79.2
2.79.1
2.79.0
2.78.4
2.78.3
2.78.2
2.78.1
2.78.0
2.77.0
2.76.5
2.76.4
2.76.3
2.76.2
2.76.1
2.76.0
2.75.0
2.74.1
2.74.0
2.73.8
2.73.7
2.73.6
2.73.5
2.73.4
2.73.3
2.73.2
2.73.1
2.73.0
2.72.5
2.72.4
2.72.3
2.72.2
2.72.1
2.72.0
2.71.1
2.71.0
2.70.0
2.69.2
2.69.1
2.69.0
2.68.11
2.68.10
Components-based design system written in elixir
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/moon.light.component.ex
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
@doc false
def run([name]) do
name
|> to_module()
|> context()
|> copy_templates()
|> print_shell_instructions()
end
defp to_module(name, pathes \\ ~w(Design Moon)) do
Module.safe_concat([name | pathes] |> Enum.reverse())
rescue
ArgumentError ->
to_module(name, tl(pathes))
end
defp context(module) do
%{
module: module,
short: module |> to_string() |> split(".") |> last(),
component_type: module.component_type(),
level: 0,
config: %{
use_translates: fn
[:Moon, :StatelessComponent] -> [:Moon, :Light, :Component]
[:Moon, :StatefulComponent] -> [:Moon, :Light, :LiveComponent]
other -> other
end,
defmodule_translates: fn
[:Moon, :Design | others] -> [:Moon, :Light | others]
[:MoonWeb, :Examples, :Design | others] -> [:MoonWeb, :Examples, :Light | others]
end,
module_translates: fn mod ->
[
:"Elixir"
| mod
|> to_string()
|> String.replace("Elixir.", "")
|> String.split(".")
|> Enum.map(&String.to_atom/1)
|> (fn
[:Moon, :Design | others] ->
[:Moon, :Light | others]
[:MoonWeb, :Examples, :Design | others] ->
[:MoonWeb, :Examples, :Light | others]
other ->
other
end).()
]
|> Enum.join(".")
|> String.to_atom()
end
}
}
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 = {:data, _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
defp translate_prop(_, {:prop, meta, [{name, _, nil}, :event]}, doc),
do: {:attr, meta, [name, Event, translate_prop_options([], doc)]}
# translate {:prop, [line: 21], [{:step, [line: 21], nil}, :integer, [default: 1]]}
# to {:attr, [line: 38], [:id, :string, [required: true]]}
defp translate_prop(_, {macro_name, meta, [{name, _, nil}, type]}, doc)
when macro_name in [:prop, :data],
do: {:attr, meta, [name, translate_prop_type(type), translate_prop_options([], doc)]}
defp translate_prop(_, {macro_name, meta, [{name, _, nil}, type, options]}, doc)
when macro_name in [:prop, :data],
do:
{:attr, meta,
[
name,
translate_prop_type(type),
translate_prop_options(options, 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, doc) do
(options
|> Enum.map(fn
{:values!, v} -> {:values, v}
{k, v} when k in [:required, :default, :values, :examples] -> {k, v}
end)) ++
((doc && [doc: doc]) || []) ++
(((Keyword.has_key?(options, :default) || Keyword.has_key?(options, :required)) && []) ||
[default: nil])
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} | attrs], _children, node_meta},
_
) do
str =
[expr | attrs |> Enum.map(&translate_slot_attr/1) |> Enum.filter(&(!!&1))]
|> Enum.join(", ")
{:expr, "render_slot(#{str})", node_meta}
end
defp translate_node({"#slot", attrs, _children, node_meta}, _) do
str =
["@inner_block" | attrs |> Enum.map(&translate_slot_attr/1) |> Enum.filter(&(!!&1))]
|> Enum.join(", ")
{:expr, "render_slot(#{str})", node_meta}
end
defp translate_node({type, attributes, children, node_meta}, c) do
{res_type, source_type} = translate_type(type, c)
{attributes, children} = pre_translate_node(source_type, {type, attributes, children}, c)
{res_type,
attributes
|> List.wrap()
|> Enum.map(&translate_attr(&1, source_type.__props__()))
|> List.flatten(), children, node_meta}
end
defp translate_node(other, _), do: other
defp pre_translate_node(Moon.Design.Table, {_, attributes, children}, c) do
{attributes, m_name} =
attributes
|> Enum.map(fn
{"items", {:attribute_expr, expr, m1}, m2} ->
[model, models] = expr |> String.split("<-") |> Enum.map(&String.trim/1)
{{"items", {:attribute_expr, models, m1}, m2}, model}
other ->
{other, nil}
end)
|> Enum.reduce({[], nil}, fn {node, model}, {nodes, cur_model} ->
{nodes ++ [node], cur_model || model}
end)
{attributes,
children
|> Enum.map(fn
{type2, attributes2, children2, meta} ->
{_, source_type} = translate_type(type2, c)
case source_type do
Moon.Design.Table.Column -> {":cols :let={#{m_name}}", attributes2, children2, meta}
_ -> {type2, attributes2, children2, meta}
end
other ->
other
end)}
end
defp pre_translate_node(_, {_, attributes, children}, _), do: {attributes, children}
defp translate_slot_attr({"generator_value", {:attribute_expr, expr, _m2}, _m1}), do: expr
defp translate_slot_attr(other) do
Logger.warning("Unknown slot attribute:")
dbg(other)
nil
end
defp get_alias(alias_, aliases) do
[key | other] = alias_ |> String.split(".")
[aliases[key] | other] |> Enum.join(".") |> String.to_atom()
end
defp translate_type(type, c = %{aliases: aliases}) do
cond do
!String.match?(type, ~r/^[A-Z].*$/) ->
{type, %{__props__: []}}
type == "Icon" ->
{".icon", Moon.Icon}
# get_alias(type, aliases) == Moon.Design.Table.Column ->
# {":cols :let={model}", Moon.Design.Table.Column}
get_alias(type, aliases) === nil ->
Logger.warning("Unknown type: #{type}")
{type, %{__props__: []}}
function_exported?(get_alias(type, aliases), :__slot_name__, 0) ->
# TODO: get subcompoent's render function
Logger.warning(
"Replacing #{get_alias(type, aliases)} with slot :#{get_alias(type, aliases).__slot_name__()}"
)
{":#{get_alias(type, aliases).__slot_name__()}", get_alias(type, aliases)}
get_alias(type, aliases).component_type() == Surface.Component ->
mod = get_alias(type, aliases) |> parent_module() |> c.config.module_translates.()
{case Code.ensure_compiled(mod) do
{:module, _} ->
func_name =
type |> String.split(".") |> Enum.at(-1) |> String.downcase() |> String.to_atom()
if Keyword.has_key?(mod.__info__(:functions), func_name) do
# TODO: add imports here
"#{(mod in [c.config.module_translates.(c.module), Moon.Light] && "") || mod}.#{func_name}"
else
".moon module={#{type}}"
end
{:error, _} ->
".moon module={#{type}}"
end, get_alias(type, aliases)}
get_alias(type, aliases).component_type() == Surface.LiveComponent ->
mod = get_alias(type, aliases) |> c.config.module_translates.()
{case Code.ensure_compiled(mod) do
{:module, _} -> ".live_component module={#{mod}}"
{:error, _} -> ".moon module={#{type}}"
end, get_alias(type, aliases)}
end
end
# defp translate_attr({name, expr, meta}), do: dbg({name, expr, meta})
defp translate_attr({name, expr, meta}, node_props) when is_atom(name),
do: translate_attr({"#{name}", expr, meta}, node_props)
defp translate_attr({name, expr, meta}, _) when name in ~w(root :let :if :for),
do: {name, expr, meta}
defp translate_attr({":attrs", expr, meta}, _), do: {:root, expr, meta}
# TODO: translate each to data-value attribute
defp translate_attr({":values", {:attribute_expr, expr, m2}, m1}, _),
do: {:root, {:attribute_expr, "data_values(#{expr})", m2}, m1}
# defp translate_attr({":values", expr, m1}),
# do: {:root, {:attribute_expr, "data_values(#{expr})", m1}, m1}
defp translate_attr({":on-" <> name, value, meta}, _) when is_binary(value) do
[
{:"phx-#{name}", value, meta},
{:"phx-target", {:attribute_expr, "@myself", meta}, meta}
]
end
defp translate_attr({":on-" <> name, {:attribute_expr, expr, m2}, m1}, _) do
[
{:"phx-#{name}", {:attribute_expr, "(#{expr}) && (#{expr}).name", m2}, m2},
{:"phx-target", {:attribute_expr, "(#{expr}) && (#{expr}).target", m2}, m1}
]
end
# defp translate_attr({":" <> name, value, meta}),
# do: {:"phx-#{name}", translate_attr_value(value), meta}
defp translate_attr({name, value, meta}, node_props) do
prop = node_props |> Enum.find(&("#{&1.name}" == "#{name}"))
if prop && prop.type == :event && is_binary(value) do
{:"#{name}", {:attribute_expr, "%Event{name: \"#{value}\", target: @myself}", meta}, meta}
else
{:"#{name}", translate_attr_value(value), meta}
end
end
defp translate_attr_value({:attribute_expr, expr, meta}) do
{:attribute_expr, expr |> translate_attr_values(), meta}
end
defp translate_attr_value(expr), do: expr
defp translate_attr_values(expr) do
case Code.string_to_quoted(expr) do
{:ok, _} -> expr
{:error, _} -> "[#{expr}]"
end
end
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(_) do
Mix.shell().info("""
""")
end
end