Current section
Files
Jump to
Current section
Files
lib/doggo/macros.ex
defmodule Doggo.Macros do
@moduledoc false
use Phoenix.Component
defmacro component(name) do
module = component_module(name)
builder_name = :"build_#{name}"
config = module.config()
opts =
Keyword.validate!(config, [
:base_class,
:extra,
:maturity,
:maturity_note,
:modifiers,
:since,
:type
])
modifiers = Keyword.fetch!(opts, :modifiers)
extra = Keyword.get(opts, :extra, [])
base_class = Keyword.get(opts, :base_class, default_base_class(name))
defaults =
[
name: opts[:name] || name,
base_class: base_class,
modifiers: modifiers,
class_name_fun: &Doggo.modifier_class_name/2
] ++ extra
type = Keyword.fetch!(opts, :type)
since = Keyword.fetch!(opts, :since)
docstring = assemble_builder_doc(module, builder_name, defaults, opts)
quote do
@doc unquote(docstring)
@doc type: unquote(type)
@doc since: unquote(since)
defmacro unquote(builder_name)(opts \\ []) do
module = unquote(module)
opts = Keyword.validate!(opts, unquote(defaults))
{opts, extra} =
Keyword.split(opts, [:name, :base_class, :modifiers, :class_name_fun])
component_info =
opts
|> Keyword.put(:component, unquote(name))
|> Keyword.put(:extra, extra)
|> Keyword.put(:type, unquote(type))
name = Keyword.fetch!(opts, :name)
modifiers = Keyword.fetch!(opts, :modifiers)
modifier_names = Keyword.keys(modifiers)
attrs_and_slots = module.attrs_and_slots()
docstring = Doggo.Macros.assemble_component_doc(module)
quote do
@dog_components unquote(component_info)
@doc unquote(docstring)
for {name, modifier_opts} <- unquote(modifiers) do
attr name, :string, modifier_opts
end
attr :class, :any,
default: [],
doc: """
Any additional classes to be added.
Variations of the component should be expressed via modifier
attributes, and it is preferable to use styles on the parent
container to arrange components on the page, but if you have to,
you can use this attribute to pass additional utility classes to
the component.
The value can be a string or a list of strings.
"""
unquote(attrs_and_slots)
def unquote(name)(var!(assigns)) do
unquote(prepare_class(opts))
unquote(module.init_block(opts, extra))
unquote(module).render(var!(assigns))
end
end
end
end
end
defp component_module(name) when is_atom(name) do
module_name = name |> Atom.to_string() |> Macro.camelize()
Module.concat([Doggo.Components, module_name])
end
defp default_base_class(name) when is_atom(name) do
name |> to_string |> String.replace("_", "-")
end
defp assemble_builder_doc(module, builder_name, defaults, opts) do
doc = module.doc()
usage = module.usage()
builder_doc =
if function_exported?(module, :builder_doc, 0) do
"""
In addition to the [common options](`m:Doggo.Components#module-common-options`)
`name`, `base_class`, `modifiers`, and `class_name_fun`, the build macro
also supports the following options.
#{module.builder_doc()}
"""
else
"""
The build macro supports the [common options](`m:Doggo.Components#module-common-options`)
`name`, `base_class`, `modifiers`, and `class_name_fun`.
"""
end
maturity = Keyword.fetch!(opts, :maturity)
maturity_note = Keyword.get(opts, :maturity_note)
maturity_info_block = build_maturity_info(maturity, maturity_note)
[
doc,
maturity_info_block,
"""
## Configuration
Generate the component with default options:
#{to_string(builder_name)}()
""",
builder_doc,
"""
### Default options
```elixir
#{inspect(defaults, pretty: true)}
```
""",
"""
## Usage
#{usage}
""",
css_example_doc(module)
]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n\n")
end
def assemble_component_doc(module) do
usage = module.usage()
doc = module.doc()
"""
#{doc}
## Usage
#{usage}
"""
end
defp css_example_doc(module) do
if function_exported?(module, :css_path, 0) do
base_url = "https://github.com/woylie/doggo/blob/main/demo/assets/css/"
url = base_url <> module.css_path()
"""
## Example CSS
For example CSS, you can have a look at the [demo styles](#{url}).
"""
end
end
defp build_maturity_info(maturity, nil) do
"""
> #### Maturity: #{maturity_to_string(maturity)} {: .info}
"""
end
defp build_maturity_info(maturity, note) do
"""
> #### Maturity: #{maturity_to_string(maturity)} {: .info}
>
#{quote_note(note)}
"""
end
defp maturity_to_string(maturity)
when maturity in [:experimental, :developing, :refining, :stable] do
maturity |> to_string() |> String.capitalize()
end
defp quote_note(note) do
note
|> String.split("\n")
|> Enum.map_join("\n", &String.trim("> #{&1}"))
end
def prepare_class(opts) do
modifiers = Keyword.fetch!(opts, :modifiers)
modifier_names = Keyword.keys(modifiers)
base_class = Keyword.fetch!(opts, :base_class)
class_name_fun = Keyword.fetch!(opts, :class_name_fun)
modifier_class_fun = modifier_class_fun(modifier_names, class_name_fun)
quote do
unquote(modifier_class_fun)
additional_classes =
if value = var!(assigns)[:class], do: List.wrap(value), else: []
unquote(combine_classes(base_class, modifier_names))
var!(assigns) =
assign(var!(assigns), base_class: unquote(base_class), class: class)
end
end
defp modifier_class_fun([], _), do: nil
defp modifier_class_fun(modifier_names, class_name_fun) do
quote do
var!(class) =
for name <- unquote(modifier_names) do
case var!(assigns) do
%{^name => value} when is_binary(value) ->
unquote(class_name_fun).(name, value)
_ ->
nil
end
end
end
end
defp combine_classes(nil, []) do
quote do
class = additional_classes
end
end
defp combine_classes(nil, _modifier_names) do
quote do
class = var!(class) ++ additional_classes
end
end
defp combine_classes(base_class, []) do
quote do
class = [unquote(base_class) | additional_classes]
end
end
defp combine_classes(base_class, _modifier_namess) do
quote do
class = [unquote(base_class) | var!(class)] ++ additional_classes
end
end
def unpack_heex(heex, extra) when is_function(heex), do: heex.(extra)
def unpack_heex(heex, _), do: heex
def unpack_component_function(fun, opts, extra) when is_function(fun),
do: fun.(opts, extra)
def unpack_component_function(fun, _, _), do: fun
end