Merge pull request #90 from mhanberg/better-parsers

Parser abstraction
This commit is contained in:
Mitchell Hanberg 2020-07-23 21:19:58 -04:00 committed by GitHub
commit 113a75a7eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 467 additions and 360 deletions

View file

@ -62,6 +62,7 @@ jobs:
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
- uses: actions/cache@v1
id: cache
with:
@ -73,7 +74,7 @@ jobs:
run: mix deps.get
- name: Run Tests
run: mix test
run: mix test || mix test --failed || mix test --failed
env:
MIX_ENV: test

View file

@ -1,5 +1,5 @@
defmodule Temple do
alias Temple.Buffer
alias Temple.Parser
@moduledoc """
> Warning: Docs are WIP
@ -123,14 +123,7 @@ defmodule Temple do
end
defmacro temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
buffer
|> Temple.Parser.Private.traverse(block)
markup = Buffer.get(buffer)
Buffer.stop(buffer)
markup = Parser.parse(block)
quote location: :keep do
unquote(markup)
@ -139,30 +132,13 @@ defmodule Temple do
defmacro temple(block) do
quote location: :keep do
import Temple
{:ok, buffer} = Buffer.start_link()
buffer
|> Temple.Parser.Private.traverse(unquote(block))
markup = Buffer.get(buffer)
Buffer.stop(buffer)
markup
Parser.parse(unquote(block))
end
end
defmacro live_temple([do: block] = _block) do
{:ok, buffer} = Buffer.start_link()
markup = Parser.parse(block)
buffer
|> Temple.Parser.Private.traverse(block)
markup = Buffer.get(buffer)
Buffer.stop(buffer)
EEx.compile_string(markup, engine: Phoenix.LiveView.Engine)
end
end

View file

@ -1,6 +1,17 @@
defmodule Temple.Parser do
@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()}
alias Temple.Buffer
@components_path Application.get_env(:temple, :components_path, "./lib/components")
@aliases Application.get_env(:temple, :aliases, [])
@ -29,6 +40,10 @@ defmodule Temple.Parser do
{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
@void_elements ~w[
meta link base
area br col embed hr img input keygen param source track wbr
@ -39,6 +54,36 @@ defmodule Temple.Parser do
{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,
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
defmodule Private do
@moduledoc false
@ -112,11 +157,17 @@ defmodule Temple.Parser do
traverse(buffer, block)
end
def traverse(buffer, {_name, _meta, _args} = original_macro) do
def traverse(buffer, [first | rest]) do
traverse(buffer, first)
traverse(buffer, rest)
end
def traverse(buffer, 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
with true <- parser.applicable?(macro),
:ok <- parser.run(macro, buffer) do
{:halt, macro}
else
{:component_applied, adjusted_macro} ->
@ -129,331 +180,5 @@ defmodule Temple.Parser do
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: :do_expressions,
applicable?: fn
{_, _, args} when is_list(args) ->
Enum.any?(args, fn arg -> match?([{:do, _} | _], arg) end)
_ ->
false
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

View file

@ -0,0 +1,58 @@
defmodule Temple.Parser.AnonymousFunctions do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({_, _, args}) do
import Temple.Parser.Private, only: [split_args: 1]
args |> split_args() |> elem(1) |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
end
def applicable?(_), do: false
@impl Parser
def run({name, _, args}, buffer) do
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
:ok
end
end

View file

@ -0,0 +1,70 @@
defmodule Temple.Parser.Components do
@behaviour Temple.Parser
@components_path Application.get_env(:temple, :components_path, "./lib/components")
alias Temple.Parser
def applicable?({name, meta, _}) when is_atom(name) do
!meta[:temple_component_applied] && File.exists?(Path.join([@components_path, "#{name}.exs"]))
end
def applicable?(_), do: false
def run({name, _meta, args}, _buffer) do
{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?(
[
Parser.nonvoid_elements(),
Parser.nonvoid_elements_aliases(),
Parser.void_elements(),
Parser.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
end

View file

@ -0,0 +1,24 @@
defmodule Temple.Parser.Default do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?(_), do: true
@impl Parser
def run({_, _, args} = macro, buffer) do
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])
:ok
end
end

View file

@ -0,0 +1,38 @@
defmodule Temple.Parser.DoExpressions do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({_, _, args}) when is_list(args) do
Enum.any?(args, fn arg -> match?([{:do, _} | _], arg) end)
end
def applicable?(_), do: false
@impl Parser
def run({name, meta, args}, buffer) do
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")
:ok
end
end

View file

@ -0,0 +1,14 @@
defmodule Temple.Parser.Empty do
@behaviour Temple.Parser
alias Temple.Parser
@impl Parser
def applicable?(ast) when ast in [nil, []], do: true
def applicable?(_), do: false
@impl Parser
def run(_ast, _buffer) do
:ok
end
end

View file

@ -0,0 +1,28 @@
defmodule Temple.Parser.Match do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({name, _, _}) do
name in [:=]
end
def applicable?(_), do: false
@impl Parser
def run({_, _, args} = macro, buffer) do
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])
:ok
end
end

View file

@ -0,0 +1,48 @@
defmodule Temple.Parser.NonvoidElementsAliases do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({name, _, _}) do
name in Parser.nonvoid_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
def run({name, _, args}, buffer) do
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 = Parser.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")
:ok
end
end

View file

@ -0,0 +1,49 @@
defmodule Temple.Parser.TempleNamespaceNonvoid do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do
name in Parser.nonvoid_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
def run({name, _, args}, buffer) do
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 = Parser.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")
:ok
end
end

View file

@ -0,0 +1,30 @@
defmodule Temple.Parser.TempleNamespaceVoid do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({{:., _, [{:__aliases__, _, [:Temple]}, name]}, _meta, _args}) do
name in Parser.void_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
def run({name, _, args}, buffer) do
import Temple.Parser.Private
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
{_do_and_else, args} =
args
|> split_args()
name = Parser.void_elements_lookup()[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
:ok
end
end

View file

@ -0,0 +1,18 @@
defmodule Temple.Parser.Text do
@behaviour Temple.Parser
alias Temple.Buffer
alias Temple.Parser
@impl Parser
def applicable?(text) when is_binary(text), do: true
def applicable?(_), do: false
@impl Parser
def run(text, buffer) do
Buffer.put(buffer, text)
Buffer.put(buffer, "\n")
:ok
end
end

View file

@ -0,0 +1,29 @@
defmodule Temple.Parser.VoidElementsAliases do
@behaviour Temple.Parser
alias Temple.Parser
alias Temple.Buffer
@impl Parser
def applicable?({name, _, _}) do
name in Parser.void_elements_aliases()
end
def applicable?(_), do: false
@impl Parser
def run({name, _, args}, buffer) do
import Temple.Parser.Private
{_do_and_else, args} =
args
|> split_args()
name = Parser.void_elements_lookup()[name]
Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
Buffer.put(buffer, "\n")
:ok
end
end

View file

@ -9,6 +9,5 @@
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [:mix], [], "hexpm", "4afc59dcadf71be7edc8b934b39f554ec7b31e2b1b1a4767383a663f86958ce3"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
}