From 4e9c7e95b48007dbafec0dd130e367f9ac56119e Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 24 Mar 2021 23:01:13 -0400 Subject: [PATCH] Generate AST --- config/test.exs | 6 ++ lib/temple/parser.ex | 32 +++---- lib/temple/parsers/anonymous_functions.ex | 21 ++++- lib/temple/parsers/default.ex | 9 +- lib/temple/parsers/do_expressions.ex | 14 ++++ lib/temple/parsers/match.ex | 7 ++ .../parsers/nonvoid_elements_aliases.ex | 30 +++++++ lib/temple/parsers/right_arrow.ex | 10 +++ lib/temple/parsers/void_elements_aliases.ex | 12 +++ test/parser/anonymous_functions_test.exs | 70 ++++++++++++++++ test/parser/default_test.exs | 33 ++++++++ test/parser/do_expressions_test.exs | 52 ++++++++++++ test/parser/match_test.exs | 43 ++++++++++ test/parser/nonvoid_elements_aliases_test.exs | 83 +++++++++++++++++++ test/parser/right_arrow_test.exs | 65 +++++++++++++++ test/parser/void_elements_aliases_test.exs | 59 +++++++++++++ 16 files changed, 528 insertions(+), 18 deletions(-) create mode 100644 test/parser/anonymous_functions_test.exs create mode 100644 test/parser/default_test.exs create mode 100644 test/parser/do_expressions_test.exs create mode 100644 test/parser/match_test.exs create mode 100644 test/parser/nonvoid_elements_aliases_test.exs create mode 100644 test/parser/right_arrow_test.exs create mode 100644 test/parser/void_elements_aliases_test.exs diff --git a/config/test.exs b/config/test.exs index 223ccbb..50b5a38 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,3 +3,9 @@ use Mix.Config # this is to make the warning go away, # Temple does not use a json_library config :phoenix, json_library: Temple + +config :temple, + aliases: [ + select: :select__, + link: :link__ + ] diff --git a/lib/temple/parser.ex b/lib/temple/parser.ex index c677efa..dad84c6 100644 --- a/lib/temple/parser.ex +++ b/lib/temple/parser.ex @@ -94,14 +94,14 @@ defmodule Temple.Parser do {_, false} <- {Text, Text.applicable?(ast)}, {_, false} <- {TempleNamespaceNonvoid, TempleNamespaceNonvoid.applicable?(ast)}, {_, false} <- {TempleNamespaceVoid, TempleNamespaceVoid.applicable?(ast)}, - {_, false} <- {Components, Components.applicable?(ast)} do - # {_, false} <- {NonvoidElementsAliases, NonvoidElementsAliases.applicable?(ast)}, - # {_, false} <- {VoidElementsAliases, VoidElementsAliases.applicable?(ast)}, - # {_, false} <- {AnonymousFunctions, AnonymousFunctions.applicable?(ast)}, - # {_, false} <- {RightArrow, RightArrow.applicable?(ast)}, - # {_, false} <- {DoExpressions, DoExpressions.applicable?(ast)}, - # {_, false} <- {Match, Match.applicable?(ast)}, - # {_, false} <- {Default, Default.applicable?(ast)} do + {_, false} <- {Components, Components.applicable?(ast)}, + {_, false} <- {NonvoidElementsAliases, NonvoidElementsAliases.applicable?(ast)}, + {_, false} <- {VoidElementsAliases, VoidElementsAliases.applicable?(ast)}, + {_, false} <- {AnonymousFunctions, AnonymousFunctions.applicable?(ast)}, + {_, false} <- {RightArrow, RightArrow.applicable?(ast)}, + {_, false} <- {DoExpressions, DoExpressions.applicable?(ast)}, + {_, false} <- {Match, Match.applicable?(ast)}, + {_, false} <- {Default, Default.applicable?(ast)} do raise "No parsers applicable!!" else {parser, true} -> @@ -185,20 +185,20 @@ defmodule Temple.Parser do {List.flatten(do_and_else), args} end - def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do - split_on_fn(rest, {args, func, args2}) + def split_on_fn([{:fn, _, _} = func | rest], {args_before, _, args_after}) do + split_on_fn(rest, {args_before, func, args_after}) end - def split_on_fn([arg | rest], {args, nil, args2}) do - split_on_fn(rest, {[arg | args], nil, args2}) + def split_on_fn([arg | rest], {args_before, nil, args_after}) do + split_on_fn(rest, {[arg | args_before], nil, args_after}) end - def split_on_fn([arg | rest], {args, func, args2}) do - split_on_fn(rest, {args, func, [arg | args2]}) + def split_on_fn([arg | rest], {args_before, func, args_after}) do + split_on_fn(rest, {args_before, func, [arg | args_after]}) end - def split_on_fn([], {args, func, args2}) do - {Enum.reverse(args), func, Enum.reverse(args2)} + def split_on_fn([], {args_before, func, args_after}) do + {Enum.reverse(args_before), func, Enum.reverse(args_after)} end def pop_compact?([]), do: {false, []} diff --git a/lib/temple/parsers/anonymous_functions.ex b/lib/temple/parsers/anonymous_functions.ex index e6a340a..51a747e 100644 --- a/lib/temple/parsers/anonymous_functions.ex +++ b/lib/temple/parsers/anonymous_functions.ex @@ -9,11 +9,30 @@ defmodule Temple.Parser.AnonymousFunctions do 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) + args + |> split_args() + |> elem(1) + |> Enum.any?(fn x -> match?({:fn, _, _}, x) end) end def applicable?(_), do: false + def run({_name, _, args} = expression) do + {_do_and_else, args} = Temple.Parser.Private.split_args(args) + + {_args, func_arg, _args2} = Temple.Parser.Private.split_on_fn(args, {[], nil, []}) + + {_func, _, [{_arrow, _, [[{_arg, _, _}], block]}]} = func_arg + + children = Temple.Parser.parse(block) + + Temple.Ast.new( + meta: %{type: :anonymous_functions}, + content: expression, + children: children + ) + end + @impl Parser def run({name, _, args}, buffer) do import Temple.Parser.Private diff --git a/lib/temple/parsers/default.ex b/lib/temple/parsers/default.ex index 249ed21..00bf9cd 100644 --- a/lib/temple/parsers/default.ex +++ b/lib/temple/parsers/default.ex @@ -6,7 +6,14 @@ defmodule Temple.Parser.Default do alias Temple.Buffer @impl Parser - def applicable?(_), do: true + def applicable?(_ast), do: true + + def run(ast) do + Temple.Ast.new( + meta: %{type: :default}, + content: ast + ) + end @impl Parser def run({_, _, args} = macro, buffer) do diff --git a/lib/temple/parsers/do_expressions.ex b/lib/temple/parsers/do_expressions.ex index a4feedd..1fe5df6 100644 --- a/lib/temple/parsers/do_expressions.ex +++ b/lib/temple/parsers/do_expressions.ex @@ -12,6 +12,20 @@ defmodule Temple.Parser.DoExpressions do def applicable?(_), do: false + def run({name, meta, args}) do + {do_and_else, args} = Temple.Parser.Private.split_args(args) + + do_body = Temple.Parser.parse(do_and_else[:do]) + + else_body = Temple.Parser.parse(do_and_else[:else]) + + Temple.Ast.new( + meta: %{type: :do_expression}, + children: [do_body, else_body], + content: {name, meta, args} + ) + end + @impl Parser def run({name, meta, args}, buffer) do import Temple.Parser.Private diff --git a/lib/temple/parsers/match.ex b/lib/temple/parsers/match.ex index e94b64c..2ec4084 100644 --- a/lib/temple/parsers/match.ex +++ b/lib/temple/parsers/match.ex @@ -12,6 +12,13 @@ defmodule Temple.Parser.Match do def applicable?(_), do: false + def run(macro) do + Temple.Ast.new( + meta: %{type: :match}, + content: macro + ) + end + @impl Parser def run({_, _, args} = macro, buffer) do import Temple.Parser.Private diff --git a/lib/temple/parsers/nonvoid_elements_aliases.ex b/lib/temple/parsers/nonvoid_elements_aliases.ex index 13f5f0f..f549141 100644 --- a/lib/temple/parsers/nonvoid_elements_aliases.ex +++ b/lib/temple/parsers/nonvoid_elements_aliases.ex @@ -12,6 +12,36 @@ defmodule Temple.Parser.NonvoidElementsAliases do def applicable?(_), do: false + def run({name, _, args}) do + name = Parser.nonvoid_elements_lookup()[name] + + {do_and_else, args} = + args + |> Temple.Parser.Private.split_args() + + {do_and_else, args} = + case args do + [args] when is_list(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 + + children = Temple.Parser.parse(do_and_else[:do]) + + Temple.Ast.new( + content: to_string(name), + meta: %{type: :nonvoid_alias}, + attrs: args, + children: children + ) + end + @impl Parser def run({name, _, args}, buffer) do import Temple.Parser.Private diff --git a/lib/temple/parsers/right_arrow.ex b/lib/temple/parsers/right_arrow.ex index 2b45ff0..cf8ff5e 100644 --- a/lib/temple/parsers/right_arrow.ex +++ b/lib/temple/parsers/right_arrow.ex @@ -9,6 +9,16 @@ defmodule Temple.Parser.RightArrow do def applicable?({:->, _, _}), do: true def applicable?(_), do: false + def run({_, _, [[pattern], args]}) do + children = Temple.Parser.parse(args) + + Temple.Ast.new( + meta: %{type: :right_arrow}, + content: pattern, + children: children + ) + end + @impl Parser def run({_, _, [[pattern], args]}, buffer) do import Temple.Parser.Private diff --git a/lib/temple/parsers/void_elements_aliases.ex b/lib/temple/parsers/void_elements_aliases.ex index 9d4e635..2ecc5d9 100644 --- a/lib/temple/parsers/void_elements_aliases.ex +++ b/lib/temple/parsers/void_elements_aliases.ex @@ -12,6 +12,18 @@ defmodule Temple.Parser.VoidElementsAliases do def applicable?(_), do: false + def run({name, _, args}) do + {_do_and_else, [args]} = Temple.Parser.Private.split_args(args) + + name = Parser.void_elements_lookup()[name] + + Temple.Ast.new( + content: name, + meta: %{type: :void_alias}, + attrs: args + ) + end + @impl Parser def run({name, _, args}, buffer) do import Temple.Parser.Private diff --git a/test/parser/anonymous_functions_test.exs b/test/parser/anonymous_functions_test.exs new file mode 100644 index 0000000..55fc2be --- /dev/null +++ b/test/parser/anonymous_functions_test.exs @@ -0,0 +1,70 @@ +defmodule Temple.Parser.AnonymousFunctionsTest do + use ExUnit.Case, async: true + + alias Temple.Parser.AnonymousFunctions + + describe "applicable?/1" do + test "returns true when the node contains an anonymous function as an argument to a function" do + raw_asts = [ + quote do + form_for changeset, Routes.foo_path(conn, :create), fn form -> + Does.something!(form) + end + end + ] + + for raw_ast <- raw_asts do + assert AnonymousFunctions.applicable?(raw_ast) + end + end + + test "returns false when the node is anything other than an anonymous function as an argument to a function" do + raw_asts = [ + quote do + Temple.div do + "foo" + end + end, + quote do + link to: "/the/route" do + "Label" + end + end + ] + + for raw_ast <- raw_asts do + refute AnonymousFunctions.applicable?(raw_ast) + end + end + end + + describe "run/2" do + test "adds a node to the buffer" do + expected_child = + quote do + Does.something!(form) + end + + raw_ast = + quote do + form_for changeset, Routes.foo_path(conn, :create), fn form -> + unquote(expected_child) + end + end + + ast = AnonymousFunctions.run(raw_ast) + + assert %Temple.Ast{ + meta: %{type: :anonymous_functions}, + content: _, + children: [ + %Temple.Ast{ + meta: %{type: :default}, + content: ^expected_child, + children: [] + } + ] + } = ast + end + end +end diff --git a/test/parser/default_test.exs b/test/parser/default_test.exs new file mode 100644 index 0000000..342ca48 --- /dev/null +++ b/test/parser/default_test.exs @@ -0,0 +1,33 @@ +defmodule Temple.Parser.DefaultTest do + use ExUnit.Case, async: true + + alias Temple.Parser.Default + + describe "applicable?/1" do + test "returns true when the node is an elixir expression" do + ast = + quote do + Foo.bar!(baz) + end + + assert Default.applicable?(ast) + end + end + + describe "run/2" do + test "adds a elixir expression node to the buffer" do + expression = + quote do + Foo.bar!(baz) + end + + ast = Default.run(expression) + + assert %Temple.Ast{ + meta: %{type: :default}, + content: expression, + children: [] + } == ast + end + end +end diff --git a/test/parser/do_expressions_test.exs b/test/parser/do_expressions_test.exs new file mode 100644 index 0000000..52badf7 --- /dev/null +++ b/test/parser/do_expressions_test.exs @@ -0,0 +1,52 @@ +defmodule Temple.Parser.DoExpressionsTest do + use ExUnit.Case, async: true + + alias Temple.Parser.DoExpressions + + describe "applicable?/1" do + test "returns true when the node contains a do expression" do + raw_ast = + quote do + for big <- boys do + "bob" + end + end + + assert DoExpressions.applicable?(raw_ast) + end + end + + describe "run/2" do + test "adds a node to the buffer" do + raw_ast = + quote do + for big <- boys do + "bob" + end + end + + ast = DoExpressions.run(raw_ast) + + assert %Temple.Ast{ + meta: %{type: :do_expression}, + content: _, + children: [ + [ + %Temple.Ast{ + meta: %{type: :text}, + content: "bob", + children: [] + } + ], + [ + %Temple.Ast{ + meta: %{type: :empty}, + content: nil, + children: [] + } + ] + ] + } = ast + end + end +end diff --git a/test/parser/match_test.exs b/test/parser/match_test.exs new file mode 100644 index 0000000..f6132d3 --- /dev/null +++ b/test/parser/match_test.exs @@ -0,0 +1,43 @@ +defmodule Temple.Parser.MatchTest do + use ExUnit.Case, async: true + + alias Temple.Parser.Match + + describe "applicable?/1" do + test "returns true when the node is an elixir match expression" do + ast = + quote do + bingo = Foo.bar!(baz) + end + + assert Match.applicable?(ast) + end + + test "returns false when the node is a anything other than an elixir match expression" do + for node <- [ + :atom, + %{key: :value}, + [] + ] do + refute Match.applicable?(node) + end + end + end + + describe "run/2" do + test "adds a elixir expression node to the buffer" do + expression = + quote do + bingo = Foo.bar!(baz) + end + + ast = Match.run(expression) + + assert %Temple.Ast{ + meta: %{type: :match}, + content: expression, + children: [] + } == ast + end + end +end diff --git a/test/parser/nonvoid_elements_aliases_test.exs b/test/parser/nonvoid_elements_aliases_test.exs new file mode 100644 index 0000000..55f1d60 --- /dev/null +++ b/test/parser/nonvoid_elements_aliases_test.exs @@ -0,0 +1,83 @@ +defmodule Temple.Parser.NonvoidElementsAliasesTest do + use ExUnit.Case, async: true + + alias Temple.Parser.NonvoidElementsAliases + + describe "applicable?/1" do + test "returns true when the node is a nonvoid element or alias" do + raw_asts = [ + quote do + div do + "foo" + end + end, + quote do + select__ do + option do + "Label" + end + end + end + ] + + for raw_ast <- raw_asts do + assert NonvoidElementsAliases.applicable?(raw_ast) + end + end + + test "returns false when the node is anything other than a nonvoid element or alias" do + raw_asts = [ + quote do + Temple.div do + "foo" + end + end, + quote do + link to: "/the/route" do + "Label" + end + end + ] + + for raw_ast <- raw_asts do + refute NonvoidElementsAliases.applicable?(raw_ast) + end + end + end + + describe "run/2" do + test "adds a node to the buffer" do + raw_ast = + quote do + div class: "foo", id: var do + select__ do + option do + "foo" + end + end + end + end + + ast = NonvoidElementsAliases.run(raw_ast) + + assert %Temple.Ast{ + meta: %{type: :nonvoid_alias}, + content: "div", + attrs: [class: "foo", id: {:var, [], _}], + children: [ + %Temple.Ast{ + content: "select", + children: [ + %Temple.Ast{ + content: "option", + children: [ + %Temple.Ast{content: "foo"} + ] + } + ] + } + ] + } = ast + end + end +end diff --git a/test/parser/right_arrow_test.exs b/test/parser/right_arrow_test.exs new file mode 100644 index 0000000..d349c7d --- /dev/null +++ b/test/parser/right_arrow_test.exs @@ -0,0 +1,65 @@ +defmodule Temple.Parser.RightArrowTest do + use ExUnit.Case, async: true + + alias Temple.Parser.RightArrow + + describe "applicable?/1" do + test "returns true when the node contains a right arrow" do + [raw_ast] = + quote do + :bar -> + :bang + end + + assert RightArrow.applicable?(raw_ast) + end + + test "returns false when the node is anything other than an anonymous function as an argument to a function" do + raw_asts = [ + quote do + Temple.div do + "foo" + end + end, + quote do + link to: "/the/route" do + "Label" + end + end + ] + + for raw_ast <- raw_asts do + refute RightArrow.applicable?(raw_ast) + end + end + end + + describe "run/2" do + test "adds a node to the buffer" do + [raw_ast] = + quote do + :bing -> + :bong + end + + bong = + quote do + :bong + end + + ast = RightArrow.run(raw_ast) + + assert %Temple.Ast{ + meta: %{type: :right_arrow}, + content: :bing, + children: [ + %Temple.Ast{ + meta: %{type: :default}, + content: ^bong, + children: [] + } + ] + } = ast + end + end +end diff --git a/test/parser/void_elements_aliases_test.exs b/test/parser/void_elements_aliases_test.exs new file mode 100644 index 0000000..707aa14 --- /dev/null +++ b/test/parser/void_elements_aliases_test.exs @@ -0,0 +1,59 @@ +defmodule Temple.Parser.VoidElementsAliasesTest do + use ExUnit.Case, async: true + + alias Temple.Parser.VoidElementsAliases + + describe "applicable?/1" do + test "returns true when the node is a nonvoid element or alias" do + raw_asts = [ + quote do + link__(src: "example.com/foo") + end, + quote do + meta content: "foo" + end + ] + + for raw_ast <- raw_asts do + assert VoidElementsAliases.applicable?(raw_ast) + end + end + + test "returns false when the node is anything other than a nonvoid element or alias" do + raw_asts = [ + quote do + Temple.div do + "foo" + end + end, + quote do + link to: "/the/route" do + "Label" + end + end + ] + + for raw_ast <- raw_asts do + refute VoidElementsAliases.applicable?(raw_ast) + end + end + end + + describe "run/2" do + test "adds a node to the buffer" do + raw_ast = + quote do + meta content: "foo" + end + + ast = VoidElementsAliases.run(raw_ast) + + assert %Temple.Ast{ + meta: %{type: :void_alias}, + content: :meta, + attrs: [content: "foo"], + children: [] + } = ast + end + end +end