This repository has been archived on 2023-08-07. You can view files and clone it, but cannot push or open issues or pull requests.
temple/lib/temple/parser.ex

203 lines
5.7 KiB
Elixir
Raw Normal View History

2020-07-10 16:32:51 +00:00
defmodule Temple.Parser do
2021-01-02 18:21:48 +00:00
@moduledoc false
@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)
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)
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,
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
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
else
"<%= Temple.Parser.Private.runtime_attrs(" <>
(attrs |> List.first() |> Macro.to_string()) <> ") %>"
2020-07-10 16:32:51 +00:00
end
end
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) ->
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
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 ->
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