2020-07-10 16:32:51 +00:00
|
|
|
defmodule Temple.Parser do
|
2021-01-02 18:21:48 +00:00
|
|
|
@moduledoc false
|
|
|
|
|
2020-07-24 00:59:10 +00:00
|
|
|
@doc """
|
|
|
|
Should return true if the parser should apply for the given AST.
|
|
|
|
"""
|
|
|
|
@callback applicable?(ast :: Macro.t()) :: boolean()
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Processes the given AST, adding the markup to the given buffer.
|
|
|
|
|
|
|
|
Should return `:ok` if the parsing pass is over, or `{:component_applied, ast}` if the pass should be restarted.
|
|
|
|
"""
|
|
|
|
@callback run(ast :: Macro.t(), buffer :: pid()) :: :ok | {:component_applied, Macro.t()}
|
|
|
|
|
2020-07-10 16:32:51 +00:00
|
|
|
alias Temple.Buffer
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
2020-07-24 00:59:10 +00:00
|
|
|
def nonvoid_elements, do: @nonvoid_elements
|
|
|
|
def nonvoid_elements_aliases, do: @nonvoid_elements_aliases
|
|
|
|
def nonvoid_elements_lookup, do: @nonvoid_elements_lookup
|
|
|
|
|
2020-07-10 16:32:51 +00:00
|
|
|
@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)
|
|
|
|
|
2020-07-24 00:59:10 +00:00
|
|
|
def void_elements, do: @void_elements
|
|
|
|
def void_elements_aliases, do: @void_elements_aliases
|
|
|
|
def void_elements_lookup, do: @void_elements_lookup
|
|
|
|
|
|
|
|
def parsers(),
|
|
|
|
do: [
|
|
|
|
Temple.Parser.Empty,
|
|
|
|
Temple.Parser.Text,
|
|
|
|
Temple.Parser.TempleNamespaceNonvoid,
|
|
|
|
Temple.Parser.TempleNamespaceVoid,
|
|
|
|
Temple.Parser.Components,
|
|
|
|
Temple.Parser.NonvoidElementsAliases,
|
|
|
|
Temple.Parser.VoidElementsAliases,
|
|
|
|
Temple.Parser.AnonymousFunctions,
|
2020-11-05 00:57:03 +00:00
|
|
|
Temple.Parser.RightArrow,
|
2020-07-24 00:59:10 +00:00
|
|
|
Temple.Parser.DoExpressions,
|
|
|
|
Temple.Parser.Match,
|
|
|
|
Temple.Parser.Default
|
|
|
|
]
|
|
|
|
|
|
|
|
def parse(ast) do
|
|
|
|
{:ok, buffer} = Buffer.start_link()
|
|
|
|
|
|
|
|
Temple.Parser.Private.traverse(buffer, ast)
|
|
|
|
markup = Buffer.get(buffer)
|
|
|
|
|
|
|
|
Buffer.stop(buffer)
|
|
|
|
|
|
|
|
markup
|
|
|
|
end
|
|
|
|
|
2020-07-10 16:32:51 +00:00
|
|
|
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
|
|
|
|
|
2020-08-09 14:07:27 +00:00
|
|
|
def compile_attrs(attrs) when is_list(attrs) do
|
|
|
|
if Keyword.keyword?(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
|
2020-07-10 16:32:51 +00:00
|
|
|
end
|
2020-08-09 14:07:27 +00:00
|
|
|
else
|
|
|
|
"<%= Temple.Parser.Private.runtime_attrs(" <>
|
|
|
|
(attrs |> List.first() |> Macro.to_string()) <> ") %>"
|
2020-07-10 16:32:51 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-08-09 14:07:27 +00:00
|
|
|
def runtime_attrs(attrs) do
|
|
|
|
{:safe,
|
|
|
|
for {name, value} <- attrs, into: "" do
|
|
|
|
name = snake_to_kebab(name)
|
|
|
|
|
|
|
|
" " <> name <> "=\"" <> to_string(value) <> "\""
|
|
|
|
end}
|
|
|
|
end
|
|
|
|
|
|
|
|
def split_args(not_what_i_want) when is_nil(not_what_i_want) or is_atom(not_what_i_want),
|
|
|
|
do: {[], []}
|
2020-07-10 16:32:51 +00:00
|
|
|
|
|
|
|
def split_args(args) do
|
|
|
|
{do_and_else, args} =
|
|
|
|
args
|
|
|
|
|> Enum.split_with(fn
|
|
|
|
arg when is_list(arg) ->
|
2020-07-16 04:19:57 +00:00
|
|
|
Keyword.keyword?(arg) && (Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
|
2020-07-10 16:32:51 +00:00
|
|
|
|
|
|
|
_ ->
|
|
|
|
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
|
|
|
|
|
2020-07-24 00:59:10 +00:00
|
|
|
def traverse(buffer, [first | rest]) do
|
|
|
|
traverse(buffer, first)
|
|
|
|
|
|
|
|
traverse(buffer, rest)
|
|
|
|
end
|
|
|
|
|
|
|
|
def traverse(buffer, original_macro) do
|
2020-07-10 16:32:51 +00:00
|
|
|
Temple.Parser.parsers()
|
Components API
Components work very similarly to how they worked before, but with a few
differences.
To define a component, you can create a file in your configured temple
components directory, which defaults to `lib/components`. You would
probably want ot change that to be `lib/my_app_web/components` if you
are building a phoenix app.
This file should be of the `.exs` extension, and contain any temple
compatible code.
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 demonstated 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
```
2020-07-16 02:11:35 +00:00
|
|
|
|> Enum.reduce_while(original_macro, fn parser, macro ->
|
2020-07-24 00:59:10 +00:00
|
|
|
with true <- parser.applicable?(macro),
|
|
|
|
:ok <- parser.run(macro, buffer) do
|
Components API
Components work very similarly to how they worked before, but with a few
differences.
To define a component, you can create a file in your configured temple
components directory, which defaults to `lib/components`. You would
probably want ot change that to be `lib/my_app_web/components` if you
are building a phoenix app.
This file should be of the `.exs` extension, and contain any temple
compatible code.
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 demonstated 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
```
2020-07-16 02:11:35 +00:00
|
|
|
{:halt, macro}
|
2020-07-10 16:32:51 +00:00
|
|
|
else
|
Components API
Components work very similarly to how they worked before, but with a few
differences.
To define a component, you can create a file in your configured temple
components directory, which defaults to `lib/components`. You would
probably want ot change that to be `lib/my_app_web/components` if you
are building a phoenix app.
This file should be of the `.exs` extension, and contain any temple
compatible code.
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 demonstated 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
```
2020-07-16 02:11:35 +00:00
|
|
|
{:component_applied, adjusted_macro} ->
|
|
|
|
traverse(buffer, adjusted_macro)
|
|
|
|
|
|
|
|
{:halt, adjusted_macro}
|
|
|
|
|
|
|
|
false ->
|
|
|
|
{:cont, macro}
|
2020-07-10 16:32:51 +00:00
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|