Skip to content

Commit

Permalink
Merge pull request #3 from lastobelus/feature/sfc.gen.init
Browse files Browse the repository at this point in the history
    feature: sfc.gen.init

    injects in `.formatter.exs`, `dev/config.exs`, and the view macro in
    `lib/my_app_web.ex`.

    optionally creates a demo card component in `lib/my_app_web/components/card.ex`,
    either single-file or with a `card.sface` template depending on
    `--template` option
  • Loading branch information
lastobelus authored Apr 25, 2021
2 parents 3bc5f84 + 98ac9c5 commit 008fb75
Show file tree
Hide file tree
Showing 11 changed files with 591 additions and 79 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.fontSize": 13,
"window.zoomLevel": -1
}
220 changes: 220 additions & 0 deletions lib/mix/sfc_gen_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,224 @@ defmodule Mix.SfcGenLive do
def generator_paths do
[".", :sfc_gen_live, :phoenix]
end

# this
def put_context_app(opts, nil) do
Keyword.put(opts, :context_app, Mix.Phoenix.context_app())
end

def put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end

def valid_namespace?(name) when is_binary(name) do
name
|> split_name()
|> valid_namespace?()
end

def valid_namespace?(namespace_parts) when is_list(namespace_parts) do
Enum.all?(namespace_parts, &valid_module?/1)
end

def split_name(name) do
name
|> Phoenix.Naming.underscore()
|> String.split("/", trim: true)
end

def inflect(namespace_parts, name, context_app \\ nil)

def inflect(namespace_parts, name, context_app) when is_nil(context_app),
do: inflect(namespace_parts, name, Mix.Phoenix.context_app())

def inflect(namespace_parts, name, context_app) when is_binary(name),
do: inflect(namespace_parts, String.split(name, "/"), context_app)

def inflect(namespace_parts, name_parts, context_app) do
path = Enum.concat(namespace_parts, name_parts) |> Enum.join("/")
web_path = Mix.Phoenix.web_path(context_app)
base = Module.concat([Mix.Phoenix.base()])
web_module = base |> Mix.Phoenix.web_module()
[lib_prefix, web_dir] = Path.split(web_path)
web_module_path = Path.join(lib_prefix, "#{web_dir}.ex")

scoped = path |> Phoenix.Naming.camelize()
namespace_module = Module.concat(namespace_parts |> Enum.map(&Phoenix.Naming.camelize/1))
module = Module.concat(web_module, scoped)
alias = Module.concat([Module.split(module) |> List.last()])
human = Enum.map(name_parts, &Phoenix.Naming.humanize/1) |> Enum.join(" ")

[
alias: alias,
human: human,
web_module: web_module,
web_module_path: web_module_path,
namespace_module: namespace_module,
module: module,
path: path,
web_path: web_path
]
end

@doc """
Finds blocks of code matching `fragment` and adds insert at position :start, :end or :after
each matching block of code IFF the block of code does not already contain insert at _any_ of
said positions.
You can also pass `:quote` for position, and the insert will be at the end of the first
**multiline** `quote do...end` block in the matching block.
Relies on the code being canonically formatted and the fragment needs to start with the
first non-whitespace character on a line it should match, or lea͠ki̧n͘g fr̶ǫm ̡yo​͟ur eye͢s̸
̛l̕ik͏e liq​uid pain, the song of re̸gular exp​ression parsing will exti​nguish the voices of
mor​tal man from the sp​here I can see it can you see ̲͚̖͔̙î̩́t̲͎̩̱͔́̋̀ it is beautiful t​he
final snuffing of the lie​s of Man ALL IS LOŚ͖̩͇̗̪̏̈́T ALL I​S LOST the pon̷y he comes he
c̶̮omes he comes the ich​or permeates all MY FACE MY FACE ᵒh god no NO NOO̼O​O NΘ stop the
an​*̶͑̾̾​̅ͫ͏̙̤g͇̫͛͆̾ͫ̑͆l͖͉̗̩̳̟̍ͫͥͨe̠̅s ͎a̧͈͖r̽̾̈́͒͑e n​ot rè̑ͧ̌aͨl̘̝̙̃ͤ͂̾̆ ZA̡͊͠͝LGΌ
ISͮ̂҉̯͈͕̹̘̱ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘
̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ
"""
def insert_in_blocks_matching_fragment(
file,
fragment,
insert,
position \\ :start,
how_many \\ :all
)

def insert_in_blocks_matching_fragment(file, fragment, insert, at, how_many)
when is_binary(fragment) do
insert_in_blocks_matching_fragment(
file,
~r/#{Regex.escape(fragment)}/,
insert,
at,
how_many
)
end

def insert_in_blocks_matching_fragment(
file,
%Regex{} = fragment,
insert,
at,
how_many
) do
# I cannot figure out why the extended regex doesn't work
# block_match = ~r/
# ^(?<indent>\ *)(?<start>
# #{Regex.escape(fragment)}
# .*\n
# )
# (?<guts>
# (?:
# (?:^\k<indent>\ +.*\n) | (?:^\s*\n)
# )*
# )
# (?<end>
# ^\k<indent>end\ *\n # this part doesn't match, don't know why!!!!
# )/mx

block_match =
~r/^(?<indent> *)(?<start>#{Regex.source(fragment)}.*\n)(?<guts>(?:(?:^\k<indent>\ +.*\n)|(?:^\s*\n))*)(?<end>^\k<indent>end *\n)/m

num_parts =
case how_many do
:all ->
:infinity

:first ->
5
end

parts =
Regex.split(
block_match,
file,
include_captures: true,
on: [:indent, :start, :guts, :end],
trim: true,
parts: num_parts
)

{start, matches} =
cond do
Regex.match?(~r/#{Regex.source(fragment)}/, Enum.at(parts, 1)) ->
{"", parts}

true ->
{hd(parts), tl(parts)}
end

Enum.join([
start,
matches
|> Enum.chunk_every(5)
|> Enum.map(fn m -> insert_in_block_matches(m, insert, at) end)
|> Enum.join()
])
end

defp insert_in_block_matches([], _insert, _at), do: ""

defp insert_in_block_matches(matches, "", _at),
do: Enum.join(matches)

defp insert_in_block_matches(matches, insert, :quote) do
Enum.join(
# ["sure but why?"] ++
# ["lets descend"] ++
# ["back out"] ++

Enum.take(matches, 2) ++
[
insert_in_blocks_matching_fragment(
Enum.at(matches, 2),
"quote do",
insert,
:end,
:first
)
] ++
Enum.slice(matches, 3..-1)
# ["dunno :("]
)
end

defp insert_in_block_matches(matches, insert, at) do
insert_index =
case at do
:start -> 2
:end -> 3
:after -> 4
end

cond do
Enum.any?(matches, fn match -> String.contains?(match, insert) end) ->
Enum.join(matches)

true ->
matches
|> List.insert_at(insert_index, indent_insert(insert, hd(matches), at))
|> Enum.join()
end
end

defp indent_insert(insert, indent, :start),
do: indent <> " " <> String.trim(insert) <> "\n\n"

defp indent_insert(insert, indent, :end),
do: "\n" <> indent <> " " <> String.trim(insert) <> "\n"

defp indent_insert(insert, indent, :after),
do: "\n" <> indent <> String.trim(insert) <> "\n"

defp valid_module?(name) do
Phoenix.Naming.camelize(name) =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end

defp inspect_string_list(strlist) do
strlist |> Enum.with_index() |> Enum.each(fn {x, i} -> IO.puts("#{i} ---------\n`#{x}`") end)
end
end
71 changes: 20 additions & 51 deletions lib/mix/tasks/sfc.gen.component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Sfc.Gen.Component do

alias Mix.Surface.Component.{Props, Slots}

@switches [template: :boolean, namespace: :string, slot: [:string, :keep]]
@switches [template: :boolean, namespace: :string, slot: [:string, :keep], context_app: :string]
@default_opts [template: false, namespace: "components"]
@doc false
def run(args) do
Expand All @@ -19,21 +19,19 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
slots = slots |> Slots.parse()

assigns =
inflect(namespace_parts, name_parts)
Mix.SfcGenLive.inflect(namespace_parts, name_parts)
|> Keyword.put(:props, props)
|> Keyword.put(:template, opts[:template])
|> Keyword.put(:slots, slots)

web_dir = Mix.Phoenix.web_path(opts[:context_app])

paths = Mix.SfcGenLive.generator_paths()

files = [
{:eex, "component.ex", Path.join(web_dir, "#{assigns[:path]}.ex")}
{:eex, "component.ex", Path.join(assigns[:web_path], "#{assigns[:path]}.ex")}
]

template_files = [
{:eex, "component.sface", Path.join(web_dir, "#{assigns[:path]}.sface")}
{:eex, "component.sface", Path.join(assigns[:web_path], "#{assigns[:path]}.sface")}
]

Mix.Phoenix.copy_from(paths, "priv/templates/sfc.gen.component", assigns, files)
Expand All @@ -48,15 +46,14 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
# def build_component(args) do
# end

@spec validate_args!(String.t(), String.t()) :: {[...], [...]}
defp validate_args!(name, namespace) do
{namespace_parts, name_parts} = normalized_component_name(name, namespace)

cond do
not Enum.all?(name_parts, &valid_module?/1) ->
not Mix.SfcGenLive.valid_namespace?(name_parts) ->
raise_with_help("Expected the component, #{inspect(name)}, to be a valid module name")

not Enum.all?(namespace_parts, &valid_module?/1) ->
not Mix.SfcGenLive.valid_namespace?(namespace_parts) ->
raise_with_help(
"Expected the namespace, #{inspect(namespace)}, to be a valid module name"
)
Expand Down Expand Up @@ -115,13 +112,23 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
Slots can be specified with `--slot` switches.
For example:
mix sfc.gen.component Hero section:string --slot default:required --slot header --slot footer[section]
mix sfc.gen.component Hero section:string \
--slot default:required \
--slot header \
--slot footer[section]
will add
slot :default, required: true
slot :header
slot :footer, values: [:section]
## Template or Sigil
By default, sfc.gen.component creates a `my_component.sface` file.
If you pass `--no-template` it will instead include a `render/1` function with
the template in a `~H` sigil.
""")
end

Expand All @@ -134,30 +141,16 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
merged_opts =
@default_opts
|> Keyword.merge(opts)
|> put_context_app(opts[:context_app])
|> Mix.SfcGenLive.put_context_app(opts[:context_app])

[name | props] = parsed
{merged_opts, slots, name, props}
end

defp put_context_app(opts, nil) do
Keyword.put(opts, :context_app, Mix.Phoenix.otp_app())
end

defp put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end

defp split_name(name) do
name
|> Phoenix.Naming.underscore()
|> String.split("/", trim: true)
end

@spec normalized_component_name(String.t(), String.t()) :: {[String.t()], [String.t()]}
defp normalized_component_name(name, namespace) do
namespace_parts = split_name(namespace)
name_parts = split_name(name) |> strip_namespace(namespace_parts)
namespace_parts = Mix.SfcGenLive.split_name(namespace)
name_parts = Mix.SfcGenLive.split_name(name) |> strip_namespace(namespace_parts)
{namespace_parts, name_parts}
end

Expand All @@ -170,28 +163,4 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
name_parts
end
end

defp valid_module?(name) do
Phoenix.Naming.camelize(name) =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end

defp inflect(namespace_parts, name_parts) do
path = Enum.concat(namespace_parts, name_parts) |> Enum.join("/")
base = Module.concat([Mix.Phoenix.base()])
web_module = base |> Mix.Phoenix.web_module()
scoped = path |> Phoenix.Naming.camelize()
namespace_module = Module.concat(namespace_parts |> Enum.map(&Phoenix.Naming.camelize/1))
module = Module.concat(web_module, scoped)
alias = Module.concat([Module.split(module) |> List.last()])
human = Enum.map(name_parts, &Phoenix.Naming.humanize/1) |> Enum.join(" ")

[
alias: alias,
human: human,
web_module: web_module,
namespace_module: namespace_module,
module: module,
path: path
]
end
end
Loading

0 comments on commit 008fb75

Please sign in to comment.