commit
3da6d275a6
17 changed files with 656 additions and 260 deletions
|
@ -2,6 +2,12 @@
|
|||
|
||||
## Master
|
||||
|
||||
## 0.6.0-alpha.2
|
||||
|
||||
### Component API
|
||||
|
||||
Please see the README for more details regarding the Component API
|
||||
|
||||
## 0.6.0-alpha.1
|
||||
|
||||
### Generators
|
||||
|
|
58
README.md
58
README.md
|
@ -56,6 +56,64 @@ temple do
|
|||
end
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
To define a component, you can create a file in your configured temple
|
||||
components directory, which defaults to `lib/components`. You would
|
||||
probably want to change that to be `lib/my_app_web/components` if you
|
||||
are building a phoenix app.
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
|
||||
config :temple, :components_path, "./lib/my_app_web/components"
|
||||
```
|
||||
|
||||
This file should be of the `.exs` extension.
|
||||
|
||||
You can then use this component in any other temple template.
|
||||
|
||||
For example, if I were to define a `flex` component, I would create a
|
||||
file called `lib/my_app_web/components/flex.exs`, with the following
|
||||
contents.
|
||||
|
||||
```elixir
|
||||
div class: "flex #{@temple[:class]}", id: @id do
|
||||
@children
|
||||
end
|
||||
```
|
||||
|
||||
And we could use the component like so
|
||||
|
||||
```elixir
|
||||
flex class: "justify-between items-center", id: "arnold" do
|
||||
div do: "Hi"
|
||||
div do: "I'm"
|
||||
div do: "Arnold"
|
||||
div do: "Schwarzenegger"
|
||||
end
|
||||
```
|
||||
|
||||
We've demonstrated several features to components in this example.
|
||||
|
||||
We can pass assigns to our component, and access them just like we would in a normal phoenix template. If they don't match up with any assigns we passed to our component, they will be rendered as-is, and will become a normal Phoenix assign.
|
||||
|
||||
You can also access a special `@temple` assign. This allows you do optionally pass an assign, and not have the `@my_assign` pass through. If you didn't pass it to your component, it will evaluate to nil.
|
||||
|
||||
The block passed to your component can be accessed as `@children`. This allows your components to wrap a body of markup from the call site.
|
||||
|
||||
In order for components to trigger a recompile when they are changed, you can call `use Temple.Recompiler` in your `lib/my_app_web.ex` file, in the `view`, `live_view`, and `live_component` functions
|
||||
|
||||
```elixir
|
||||
def view do
|
||||
quote do
|
||||
# ...
|
||||
use Temple.Recompiler
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Phoenix templates
|
||||
|
||||
Add the templating engine to your Phoenix configuration.
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
use Mix.Config
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
1
config/dev.exs
Normal file
1
config/dev.exs
Normal file
|
@ -0,0 +1 @@
|
|||
use Mix.Config
|
3
config/test.exs
Normal file
3
config/test.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use Mix.Config
|
||||
|
||||
config :temple, components_path: "./test/support/components"
|
|
@ -152,10 +152,10 @@ if Code.ensure_loaded?(Mix.Phoenix) do
|
|||
end
|
||||
|
||||
defp copy_new_files(%Context{} = context, binding, paths) do
|
||||
files = files_to_be_generated(context) |> IO.inspect(label: "FILES")
|
||||
files = files_to_be_generated(context)
|
||||
|
||||
Mix.Phoenix.copy_from(
|
||||
paths |> IO.inspect(label: "PATHS"),
|
||||
paths,
|
||||
"priv/templates/temple.gen.live",
|
||||
binding,
|
||||
files
|
||||
|
|
261
lib/temple.ex
261
lib/temple.ex
|
@ -122,266 +122,11 @@ defmodule Temple do
|
|||
end
|
||||
end
|
||||
|
||||
defmodule Private do
|
||||
@moduledoc false
|
||||
@aliases Application.get_env(:temple, :aliases, [])
|
||||
|
||||
@nonvoid_elements ~w[
|
||||
head title style script
|
||||
noscript template
|
||||
body section nav article aside h1 h2 h3 h4 h5 h6
|
||||
header footer address main
|
||||
p pre blockquote ol ul li dl dt dd figure figcaption div
|
||||
a em strong small s cite q dfn abbr data time code var samp kbd
|
||||
sub sup i b u mark ruby rt rp bdi bdo span
|
||||
ins del
|
||||
iframe object video audio canvas
|
||||
map
|
||||
table caption colgroup tbody thead tfoot tr td th
|
||||
form fieldset legend label button select datalist optgroup
|
||||
option textarea output progress meter
|
||||
details summary menuitem menu
|
||||
html
|
||||
]a
|
||||
|
||||
@nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
|
||||
Keyword.get(@aliases, el, el)
|
||||
end)
|
||||
@nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
|
||||
{Keyword.get(@aliases, el, el), el}
|
||||
end)
|
||||
|
||||
@void_elements ~w[
|
||||
meta link base
|
||||
area br col embed hr img input keygen param source track wbr
|
||||
]a
|
||||
|
||||
@void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
|
||||
@void_elements_lookup Enum.map(@void_elements, fn el ->
|
||||
{Keyword.get(@aliases, el, el), el}
|
||||
end)
|
||||
|
||||
def snake_to_kebab(stringable),
|
||||
do:
|
||||
stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
|
||||
|
||||
def kebab_to_snake(stringable),
|
||||
do: stringable |> to_string() |> String.replace("-", "_")
|
||||
|
||||
def compile_attrs([]), do: ""
|
||||
|
||||
def compile_attrs([attrs]) when is_list(attrs) do
|
||||
compile_attrs(attrs)
|
||||
end
|
||||
|
||||
def compile_attrs(attrs) do
|
||||
for {name, value} <- attrs, into: "" do
|
||||
name = snake_to_kebab(name)
|
||||
|
||||
case value do
|
||||
{_, _, _} = macro ->
|
||||
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
|
||||
|
||||
value ->
|
||||
" " <> name <> "=\"" <> to_string(value) <> "\""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def split_args(nil), do: {[], []}
|
||||
|
||||
def split_args(args) do
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> Enum.split_with(fn
|
||||
arg when is_list(arg) ->
|
||||
(Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
{List.flatten(do_and_else), args}
|
||||
end
|
||||
|
||||
def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
|
||||
split_on_fn(rest, {args, func, args2})
|
||||
end
|
||||
|
||||
def split_on_fn([arg | rest], {args, nil, args2}) do
|
||||
split_on_fn(rest, {[arg | args], nil, args2})
|
||||
end
|
||||
|
||||
def split_on_fn([arg | rest], {args, func, args2}) do
|
||||
split_on_fn(rest, {args, func, [arg | args2]})
|
||||
end
|
||||
|
||||
def split_on_fn([], {args, func, args2}) do
|
||||
{Enum.reverse(args), func, Enum.reverse(args2)}
|
||||
end
|
||||
|
||||
def pop_compact?([]), do: {false, []}
|
||||
def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
|
||||
|
||||
def pop_compact?(args) do
|
||||
Keyword.pop(args, :compact, false)
|
||||
end
|
||||
|
||||
def traverse(buffer, {:__block__, _meta, block}) do
|
||||
traverse(buffer, block)
|
||||
end
|
||||
|
||||
def traverse(buffer, {name, meta, args} = macro) do
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
includes_fn? = args |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
|
||||
|
||||
case name do
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @nonvoid_elements_aliases ->
|
||||
{do_and_else, args} =
|
||||
case args do
|
||||
[args] ->
|
||||
{do_value, args} = Keyword.pop(args, :do)
|
||||
|
||||
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
|
||||
|
||||
{do_and_else, args}
|
||||
|
||||
_ ->
|
||||
{do_and_else, args}
|
||||
end
|
||||
|
||||
name = @nonvoid_elements_lookup[name]
|
||||
|
||||
{compact?, args} = pop_compact?(args)
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
unless compact?, do: Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
if compact?, do: Buffer.remove_new_line(buffer)
|
||||
Buffer.put(buffer, "</#{name}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @void_elements_aliases ->
|
||||
name = @void_elements_lookup[name]
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
name when name in @nonvoid_elements_aliases ->
|
||||
{do_and_else, args} =
|
||||
case args do
|
||||
[args] ->
|
||||
{do_value, args} = Keyword.pop(args, :do)
|
||||
|
||||
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
|
||||
|
||||
{do_and_else, args}
|
||||
|
||||
_ ->
|
||||
{do_and_else, args}
|
||||
end
|
||||
|
||||
name = @nonvoid_elements_lookup[name]
|
||||
|
||||
{compact?, args} = pop_compact?(args)
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
unless compact?, do: Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
if compact?, do: Buffer.remove_new_line(buffer)
|
||||
Buffer.put(buffer, "</#{name}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
name when name in @void_elements_aliases ->
|
||||
name = @void_elements_lookup[name]
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
name when includes_fn? ->
|
||||
{args, func_arg, args2} = split_on_fn(args, {[], nil, []})
|
||||
|
||||
{func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
|
||||
|
||||
Buffer.put(
|
||||
buffer,
|
||||
"<%= " <>
|
||||
to_string(name) <>
|
||||
" " <>
|
||||
(Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
|
||||
", " <>
|
||||
to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
|
||||
)
|
||||
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
traverse(buffer, block)
|
||||
|
||||
if Enum.any?(args2) do
|
||||
Buffer.put(
|
||||
buffer,
|
||||
"<% end, " <>
|
||||
(Enum.map(args2, fn arg -> Macro.to_string(arg) end)
|
||||
|> Enum.join(", ")) <> " %>"
|
||||
)
|
||||
|
||||
Buffer.put(buffer, "\n")
|
||||
else
|
||||
Buffer.put(buffer, "<% end %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
|
||||
name when name in [:for, :if, :unless] ->
|
||||
Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
traverse(buffer, do_and_else[:do])
|
||||
|
||||
if Keyword.has_key?(do_and_else, :else) do
|
||||
Buffer.put(buffer, "<% else %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:else])
|
||||
end
|
||||
|
||||
Buffer.put(buffer, "<% end %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
name when name in [:=] ->
|
||||
Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
|
||||
_ ->
|
||||
Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
end
|
||||
end
|
||||
|
||||
def traverse(buffer, [first | rest]) do
|
||||
traverse(buffer, first)
|
||||
|
||||
traverse(buffer, rest)
|
||||
end
|
||||
|
||||
def traverse(buffer, text) when is_binary(text) do
|
||||
Buffer.put(buffer, text)
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
|
||||
def traverse(_buffer, arg) when arg in [nil, []] do
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defmacro temple([do: block] = _block) do
|
||||
{:ok, buffer} = Buffer.start_link()
|
||||
|
||||
buffer
|
||||
|> Temple.Private.traverse(block)
|
||||
|> Temple.Parser.Private.traverse(block)
|
||||
|
||||
markup = Buffer.get(buffer)
|
||||
|
||||
|
@ -399,7 +144,7 @@ defmodule Temple do
|
|||
{:ok, buffer} = Buffer.start_link()
|
||||
|
||||
buffer
|
||||
|> Temple.Private.traverse(unquote(block))
|
||||
|> Temple.Parser.Private.traverse(unquote(block))
|
||||
|
||||
markup = Buffer.get(buffer)
|
||||
|
||||
|
@ -413,7 +158,7 @@ defmodule Temple do
|
|||
{:ok, buffer} = Buffer.start_link()
|
||||
|
||||
buffer
|
||||
|> Temple.Private.traverse(block)
|
||||
|> Temple.Parser.Private.traverse(block)
|
||||
|
||||
markup = Buffer.get(buffer)
|
||||
|
||||
|
|
455
lib/temple/parser.ex
Normal file
455
lib/temple/parser.ex
Normal file
|
@ -0,0 +1,455 @@
|
|||
defmodule Temple.Parser do
|
||||
alias Temple.Buffer
|
||||
@components_path Application.get_env(:temple, :components_path, "./lib/components")
|
||||
|
||||
@aliases Application.get_env(:temple, :aliases, [])
|
||||
|
||||
@nonvoid_elements ~w[
|
||||
head title style script
|
||||
noscript template
|
||||
body section nav article aside h1 h2 h3 h4 h5 h6
|
||||
header footer address main
|
||||
p pre blockquote ol ul li dl dt dd figure figcaption div
|
||||
a em strong small s cite q dfn abbr data time code var samp kbd
|
||||
sub sup i b u mark ruby rt rp bdi bdo span
|
||||
ins del
|
||||
iframe object video audio canvas
|
||||
map
|
||||
table caption colgroup tbody thead tfoot tr td th
|
||||
form fieldset legend label button select datalist optgroup
|
||||
option textarea output progress meter
|
||||
details summary menuitem menu
|
||||
html
|
||||
]a
|
||||
|
||||
@nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
|
||||
Keyword.get(@aliases, el, el)
|
||||
end)
|
||||
@nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
|
||||
{Keyword.get(@aliases, el, el), el}
|
||||
end)
|
||||
|
||||
@void_elements ~w[
|
||||
meta link base
|
||||
area br col embed hr img input keygen param source track wbr
|
||||
]a
|
||||
|
||||
@void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
|
||||
@void_elements_lookup Enum.map(@void_elements, fn el ->
|
||||
{Keyword.get(@aliases, el, el), el}
|
||||
end)
|
||||
|
||||
defmodule Private do
|
||||
@moduledoc false
|
||||
|
||||
def snake_to_kebab(stringable),
|
||||
do:
|
||||
stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
|
||||
|
||||
def kebab_to_snake(stringable),
|
||||
do: stringable |> to_string() |> String.replace("-", "_")
|
||||
|
||||
def compile_attrs([]), do: ""
|
||||
|
||||
def compile_attrs([attrs]) when is_list(attrs) do
|
||||
compile_attrs(attrs)
|
||||
end
|
||||
|
||||
def compile_attrs(attrs) do
|
||||
for {name, value} <- attrs, into: "" do
|
||||
name = snake_to_kebab(name)
|
||||
|
||||
case value do
|
||||
{_, _, _} = macro ->
|
||||
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
|
||||
|
||||
value ->
|
||||
" " <> name <> "=\"" <> to_string(value) <> "\""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def split_args(nil), do: {[], []}
|
||||
|
||||
def split_args(args) do
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> Enum.split_with(fn
|
||||
arg when is_list(arg) ->
|
||||
(Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
{List.flatten(do_and_else), args}
|
||||
end
|
||||
|
||||
def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
|
||||
split_on_fn(rest, {args, func, args2})
|
||||
end
|
||||
|
||||
def split_on_fn([arg | rest], {args, nil, args2}) do
|
||||
split_on_fn(rest, {[arg | args], nil, args2})
|
||||
end
|
||||
|
||||
def split_on_fn([arg | rest], {args, func, args2}) do
|
||||
split_on_fn(rest, {args, func, [arg | args2]})
|
||||
end
|
||||
|
||||
def split_on_fn([], {args, func, args2}) do
|
||||
{Enum.reverse(args), func, Enum.reverse(args2)}
|
||||
end
|
||||
|
||||
def pop_compact?([]), do: {false, []}
|
||||
def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
|
||||
|
||||
def pop_compact?(args) do
|
||||
Keyword.pop(args, :compact, false)
|
||||
end
|
||||
|
||||
def traverse(buffer, {:__block__, _meta, block}) do
|
||||
traverse(buffer, block)
|
||||
end
|
||||
|
||||
def traverse(buffer, {_name, _meta, _args} = original_macro) do
|
||||
Temple.Parser.parsers()
|
||||
|> Enum.reduce_while(original_macro, fn parser, macro ->
|
||||
with true <- parser.applicable?.(macro),
|
||||
:ok <- parser.parse.(macro, buffer) do
|
||||
{:halt, macro}
|
||||
else
|
||||
{:component_applied, adjusted_macro} ->
|
||||
traverse(buffer, adjusted_macro)
|
||||
|
||||
{:halt, adjusted_macro}
|
||||
|
||||
false ->
|
||||
{:cont, macro}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def traverse(buffer, [first | rest]) do
|
||||
traverse(buffer, first)
|
||||
|
||||
traverse(buffer, rest)
|
||||
end
|
||||
|
||||
def traverse(buffer, text) when is_binary(text) do
|
||||
Buffer.put(buffer, text)
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def traverse(_buffer, arg) when arg in [nil, []] do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def parsers(),
|
||||
do: [
|
||||
%{
|
||||
name: :temple_namespace_nonvoid,
|
||||
applicable?: fn {name, _meta, _args} ->
|
||||
try do
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
|
||||
name in @nonvoid_elements_aliases
|
||||
rescue
|
||||
MatchError ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
parse: fn {name, _meta, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
|
||||
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
{do_and_else, args} =
|
||||
case args do
|
||||
[args] ->
|
||||
{do_value, args} = Keyword.pop(args, :do)
|
||||
|
||||
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
|
||||
|
||||
{do_and_else, args}
|
||||
|
||||
_ ->
|
||||
{do_and_else, args}
|
||||
end
|
||||
|
||||
name = @nonvoid_elements_lookup[name]
|
||||
|
||||
{compact?, args} = pop_compact?(args)
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
unless compact?, do: Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
if compact?, do: Buffer.remove_new_line(buffer)
|
||||
Buffer.put(buffer, "</#{name}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :temple_namespace_void,
|
||||
applicable?: fn {name, _meta, _args} ->
|
||||
try do
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
|
||||
name in @void_elements_aliases
|
||||
rescue
|
||||
MatchError ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
parse: fn {name, _, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
|
||||
|
||||
{_do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
name = @void_elements_lookup[name]
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :components,
|
||||
applicable?: fn {name, meta, _} ->
|
||||
try do
|
||||
!meta[:temple_component_applied] &&
|
||||
File.exists?(Path.join([@components_path, "#{name}.exs"]))
|
||||
rescue
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
parse: fn {name, _meta, args}, _buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{assigns, children} =
|
||||
case args do
|
||||
[assigns, [do: block]] ->
|
||||
{assigns, block}
|
||||
|
||||
[[do: block]] ->
|
||||
{nil, block}
|
||||
|
||||
[assigns] ->
|
||||
{assigns, nil}
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
ast =
|
||||
File.read!(Path.join([@components_path, "#{name}.exs"]))
|
||||
|> Code.string_to_quoted!()
|
||||
|
||||
{name, meta, args} =
|
||||
ast
|
||||
|> Macro.prewalk(fn
|
||||
{:@, _, [{:children, _, _}]} ->
|
||||
children
|
||||
|
||||
{:@, _, [{:temple, _, _}]} ->
|
||||
assigns
|
||||
|
||||
{:@, _, [{name, _, _}]} = node ->
|
||||
if !is_nil(assigns) && name in Keyword.keys(assigns) do
|
||||
Keyword.get(assigns, name, nil)
|
||||
else
|
||||
node
|
||||
end
|
||||
|
||||
node ->
|
||||
node
|
||||
end)
|
||||
|
||||
ast =
|
||||
if Enum.any?(
|
||||
[
|
||||
@nonvoid_elements,
|
||||
@nonvoid_elements_aliases,
|
||||
@void_elements,
|
||||
@void_elements_aliases
|
||||
],
|
||||
fn elements -> name in elements end
|
||||
) do
|
||||
{name, Keyword.put(meta, :temple_component_applied, true), args}
|
||||
else
|
||||
{name, meta, args}
|
||||
end
|
||||
|
||||
{:component_applied, ast}
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :nonvoid_elements_aliases,
|
||||
applicable?: fn {name, _, _} ->
|
||||
name in @nonvoid_elements_aliases
|
||||
end,
|
||||
parse: fn {name, _, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
{do_and_else, args} =
|
||||
case args do
|
||||
[args] ->
|
||||
{do_value, args} = Keyword.pop(args, :do)
|
||||
|
||||
do_and_else = Keyword.put_new(do_and_else, :do, do_value)
|
||||
|
||||
{do_and_else, args}
|
||||
|
||||
_ ->
|
||||
{do_and_else, args}
|
||||
end
|
||||
|
||||
name = @nonvoid_elements_lookup[name]
|
||||
|
||||
{compact?, args} = pop_compact?(args)
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
unless compact?, do: Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
if compact?, do: Buffer.remove_new_line(buffer)
|
||||
Buffer.put(buffer, "</#{name}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :void_elements_aliases,
|
||||
applicable?: fn {name, _, _} ->
|
||||
name in @void_elements_aliases
|
||||
end,
|
||||
parse: fn {name, _, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{_do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
name = @void_elements_lookup[name]
|
||||
|
||||
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :anonymous_functions,
|
||||
applicable?: fn {_, _, args} ->
|
||||
import Temple.Parser.Private, only: [split_args: 1]
|
||||
|
||||
args |> split_args() |> elem(1) |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
|
||||
end,
|
||||
parse: fn {name, _, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{_do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
{args, func_arg, args2} = split_on_fn(args, {[], nil, []})
|
||||
|
||||
{func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
|
||||
|
||||
Buffer.put(
|
||||
buffer,
|
||||
"<%= " <>
|
||||
to_string(name) <>
|
||||
" " <>
|
||||
(Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
|
||||
", " <>
|
||||
to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
|
||||
)
|
||||
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
traverse(buffer, block)
|
||||
|
||||
if Enum.any?(args2) do
|
||||
Buffer.put(
|
||||
buffer,
|
||||
"<% end, " <>
|
||||
(Enum.map(args2, fn arg -> Macro.to_string(arg) end)
|
||||
|> Enum.join(", ")) <> " %>"
|
||||
)
|
||||
|
||||
Buffer.put(buffer, "\n")
|
||||
else
|
||||
Buffer.put(buffer, "<% end %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :for_if_unless,
|
||||
applicable?: fn {name, _, _} ->
|
||||
name in [:for, :if, :unless]
|
||||
end,
|
||||
parse: fn {name, meta, args}, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{do_and_else, args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
traverse(buffer, do_and_else[:do])
|
||||
|
||||
if Keyword.has_key?(do_and_else, :else) do
|
||||
Buffer.put(buffer, "<% else %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:else])
|
||||
end
|
||||
|
||||
Buffer.put(buffer, "<% end %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :match,
|
||||
applicable?: fn {name, _, _} ->
|
||||
name in [:=]
|
||||
end,
|
||||
parse: fn {_, _, args} = macro, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{do_and_else, _args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :default,
|
||||
applicable?: fn _ -> true end,
|
||||
parse: fn {_, _, args} = macro, buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{do_and_else, _args} =
|
||||
args
|
||||
|> split_args()
|
||||
|
||||
Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
|
||||
Buffer.put(buffer, "\n")
|
||||
traverse(buffer, do_and_else[:do])
|
||||
end
|
||||
}
|
||||
]
|
||||
end
|
15
lib/temple/recompiler.ex
Normal file
15
lib/temple/recompiler.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Temple.Recompiler do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
component_path = Application.get_env(:temple, :components_path)
|
||||
|
||||
for f <- File.ls!(component_path),
|
||||
do:
|
||||
Module.put_attribute(
|
||||
__MODULE__,
|
||||
:external_resource,
|
||||
Path.join(component_path, f)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
16
test/partial_test.exs
Normal file
16
test/partial_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule PartialTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Temple
|
||||
use Temple.Support.Utils
|
||||
|
||||
test "can correctly redefine elements" do
|
||||
result =
|
||||
temple do
|
||||
section do
|
||||
"Howdy!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<section class="foo!">Howdy!</section>}
|
||||
end
|
||||
end
|
3
test/support/components/component.exs
Normal file
3
test/support/components/component.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @assign do
|
||||
@children
|
||||
end
|
3
test/support/components/component2.exs
Normal file
3
test/support/components/component2.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @class do
|
||||
@children
|
||||
end
|
3
test/support/components/has_temple.exs
Normal file
3
test/support/components/has_temple.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @temple[:class] do
|
||||
@children
|
||||
end
|
3
test/support/components/inner.exs
Normal file
3
test/support/components/inner.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div id: "inner", outer_id: @outer_id do
|
||||
@children
|
||||
end
|
3
test/support/components/outer.exs
Normal file
3
test/support/components/outer.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
inner outer_id: "from-outer" do
|
||||
@children
|
||||
end
|
3
test/support/components/section.exs
Normal file
3
test/support/components/section.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
section class: "foo!" do
|
||||
@children
|
||||
end
|
|
@ -265,4 +265,81 @@ defmodule TempleTest do
|
|||
|
||||
assert result == ~s{<p>Bob</p>\n<p><%= foo %></p>}
|
||||
end
|
||||
|
||||
test "inlines function components" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
component do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><div class="<%= @assign %>">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can accept local assigns" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
component2 class: "bg-red" do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><div class="bg-red">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can accept local assigns that are variables" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
class = "bg-red"
|
||||
|
||||
component2 class: class do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><% class = "bg-red" %><div class="<%= class %>">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can use other components" do
|
||||
result =
|
||||
temple do
|
||||
outer do
|
||||
"outer!"
|
||||
end
|
||||
|
||||
inner do
|
||||
"inner!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div id="inner" outer-id="from-outer">outer!</div><div id="inner" outer-id="<%= @outer_id %>">inner!</div>}
|
||||
end
|
||||
|
||||
test "@temple should be available in any component" do
|
||||
result =
|
||||
temple do
|
||||
has_temple class: "boom" do
|
||||
"yay!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<div class="<%= [class: "boom"][:class] %>">yay!</div>}
|
||||
end
|
||||
end
|
||||
|
|
Reference in a new issue