From c4080bf2c1167514f07b759d54d0ff8dcdda69c4 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 4 Jan 2025 17:25:54 -0500 Subject: [PATCH] Introduce LiveViewNative.Template.Engine Move Template Engine functions into it's own module --- CHANGELOG.md | 4 +- config/test.exs | 2 +- lib/live_view_native/component.ex | 4 +- lib/live_view_native/engine.ex | 29 +-- lib/live_view_native/tag_engine.ex | 183 --------------- lib/live_view_native/template/engine.ex | 213 ++++++++++++++++++ lib/mix/tasks/lvn.setup.config.ex | 2 +- .../integrations/lvn_template_test.exs | 6 +- .../engine_test.exs} | 18 +- test/mix/tasks/lvn.setup_test.exs | 4 +- test/support/clients/gameboy.ex | 2 +- test/support/clients/switch.ex | 2 +- 12 files changed, 245 insertions(+), 224 deletions(-) create mode 100644 lib/live_view_native/template/engine.ex rename test/live_view_native/{tag_engine_test.exs => template/engine_test.exs} (61%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c999b41d..3f703dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - :interface- special attribute support in tags - async_result/1 - render_upload support in LiveViewNativeTest -- suppport single quotes to wrap attribute values in template parser +- support single quotes to wrap attribute values in template parser - LVN Commands +- LiveViewNative.Template.Engine ### Changed - `LiveViewNative.Component` no longer imports `Phoenix.Component.to_form/2` - `LiveViewNative.LiveView` now requires the `dispatch_to` function to determine which module will be used for rendering +- Migrated many functions out of LiveViewNative.TagEngine to LiveViewNative.Template.Engine ### Fixed diff --git a/config/test.exs b/config/test.exs index 207a4aea..c7c4895d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -16,7 +16,7 @@ config :phoenix_template, format_encoders: [ ] config :phoenix, template_engines: [ - neex: LiveViewNative.Engine + neex: LiveViewNative.Template.Engine ] config :live_view_native_test_endpoint, diff --git a/lib/live_view_native/component.ex b/lib/live_view_native/component.ex index 942ca9c9..a9613ff2 100644 --- a/lib/live_view_native/component.ex +++ b/lib/live_view_native/component.ex @@ -292,13 +292,13 @@ defmodule LiveViewNative.Component do end options = [ - engine: LiveViewNative.TagEngine, # Phoenix.LiveView.TagEngine, + engine: LiveViewNative.TagEngine, file: __CALLER__.file, line: __CALLER__.line + 1, caller: __CALLER__, indentation: meta[:indentation] || 0, source: expr, - tag_handler: LiveViewNative.TagEngine + tag_handler: LiveViewNative.Template.Engine ] EEx.compile_string(expr, options) diff --git a/lib/live_view_native/engine.ex b/lib/live_view_native/engine.ex index 9cdd6612..46b19712 100644 --- a/lib/live_view_native/engine.ex +++ b/lib/live_view_native/engine.ex @@ -9,30 +9,19 @@ defmodule LiveViewNative.Engine do @behaviour Phoenix.Template.Engine @impl true - def compile(path, _name) do - quote do - require LiveViewNative.Engine - LiveViewNative.Engine.compile(unquote(path)) - end - end + def compile(path, name) do + IO.warn(""" + LiveViewNative.Engine has been deprecatd in favor of LiveViewNative.Template.Engine. + In config/config.exs update config :phoenix, :template_engines - @doc false - defmacro compile(path) do - trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true) - source = File.read!(path) + - neex: LiveViewNative.Engine + + neex: LiveViewNative.Template.Engine + """) - EEx.compile_string(source, - engine: Phoenix.LiveView.TagEngine, - line: 1, - file: path, - trim: trim, - caller: __CALLER__, - source: source, - tag_handler: LiveViewNative.TagEngine - ) + LiveViewNative.Template.Engine.compile(path, name) end - @doc """ + @doc """ Encodes the HTML templates to iodata. """ def encode_to_iodata!({:safe, body}), do: body diff --git a/lib/live_view_native/tag_engine.ex b/lib/live_view_native/tag_engine.ex index 77726db3..fae6d1aa 100644 --- a/lib/live_view_native/tag_engine.ex +++ b/lib/live_view_native/tag_engine.ex @@ -4,7 +4,6 @@ defmodule LiveViewNative.TagEngine do """ alias Phoenix.LiveView.Tokenizer - @behaviour Phoenix.LiveView.TagEngine @behaviour EEx.Engine @impl true @@ -79,186 +78,4 @@ defmodule LiveViewNative.TagEngine do def handle_end(state) do Phoenix.LiveView.TagEngine.handle_end(state) end - - @doc false - @impl true - def handle_attributes(ast, meta) do - if is_list(ast) and literal_keys?(ast) do - attrs = - Enum.map(ast, fn {key, value} -> - name = to_string(key) - - case handle_attr_escape(name, value, meta) do - :error -> handle_attrs_escape([{safe_unless_special(name), value}], meta) - parts -> {name, parts} - end - end) - - {:attributes, attrs} - else - {:quoted, handle_attrs_escape(ast, meta)} - end - end - - @doc false - @impl true - defdelegate annotate_body(caller), to: Phoenix.LiveView.HTMLEngine - - @doc false - @impl true - defdelegate annotate_caller(file, line), to: Phoenix.LiveView.HTMLEngine - - @doc false - @impl true - def classify_type(":inner_block"), do: {:error, "the slot name :inner_block is reserved"} - def classify_type(":" <> name), do: {:slot, name} - - def classify_type(<> = name) when first in ?A..?Z do - if String.contains?(name, ".") do - {:remote_component, name} - else - {:tag, name} - end - end - - def classify_type("." <> name), - do: {:local_component, name} - - def classify_type(name), do: {:tag, name} - - defp literal_keys?([{key, _value} | rest]) when is_atom(key) or is_binary(key), - do: literal_keys?(rest) - - defp literal_keys?([]), do: true - defp literal_keys?(_other), do: false - - defp handle_attrs_escape(attrs, meta) do - quote line: meta[:line] do - unquote(__MODULE__).attributes_escape(unquote(attrs)) - end - end - - defp handle_attr_escape("class", [head | tail], meta) when is_binary(head) do - {bins, tail} = Enum.split_while(tail, &is_binary/1) - encoded = class_attribute_encode([head | bins]) - - if tail == [] do - [IO.iodata_to_binary(encoded)] - else - tail = - quote line: meta[:line] do - {:safe, unquote(__MODULE__).class_attribute_encode(unquote(tail))} - end - - [IO.iodata_to_binary([encoded, ?\s]), tail] - end - end - - defp handle_attr_escape("class", value, meta) do - [ - quote( - line: meta[:line], - do: {:safe, unquote(__MODULE__).class_attribute_encode(unquote(value))} - ) - ] - end - - defp handle_attr_escape(_name, value, meta) do - case extract_binaries(value, true, [], meta) do - :error -> :error - reversed -> Enum.reverse(reversed) - end - end - - defp extract_binaries({:<>, _, [left, right]}, _root?, acc, meta) do - extract_binaries(right, false, extract_binaries(left, false, acc, meta), meta) - end - - defp extract_binaries({:<<>>, _, parts} = binary, _root?, acc, meta) do - Enum.reduce(parts, acc, fn - part, acc when is_binary(part) -> - [binary_encode(part) | acc] - - {:"::", _, [binary, {:binary, _, _}]}, acc -> - [quoted_binary_encode(binary, meta) | acc] - - _, _ -> - throw(:unknown_part) - end) - catch - :unknown_part -> - [quoted_binary_encode(binary, meta) | acc] - end - - defp extract_binaries(binary, _root?, acc, _meta) when is_binary(binary), - do: [binary_encode(binary) | acc] - - defp extract_binaries(value, false, acc, meta), - do: [quoted_binary_encode(value, meta) | acc] - - defp extract_binaries(_value, true, _acc, _meta), - do: :error - - @doc false - def attributes_escape(attrs) do - attrs - |> Enum.map(fn - {key, value} when is_atom(key) -> {Atom.to_string(key), value} - other -> other - end) - |> LiveViewNative.Template.attributes_escape() - end - - @doc false - def class_attribute_encode(list) when is_list(list), - do: list |> class_attribute_list() |> LiveViewNative.Engine.encode_to_iodata!() - - def class_attribute_encode(other), - do: empty_attribute_encode(other) - - defp class_attribute_list(value) do - value - |> Enum.flat_map(fn - nil -> [] - false -> [] - inner when is_list(inner) -> [class_attribute_list(inner)] - other -> [other] - end) - |> Enum.join(" ") - end - - @doc false - def empty_attribute_encode(nil), do: "" - def empty_attribute_encode(false), do: "" - def empty_attribute_encode(true), do: "" - def empty_attribute_encode(value), do: LiveViewNative.Engine.encode_to_iodata!(value) - - @doc false - def binary_encode(value) when is_binary(value) do - value - |> LiveViewNative.Engine.encode_to_iodata!() - |> IO.iodata_to_binary() - end - - def binary_encode(value) do - raise ArgumentError, "expected a binary in <>, got: #{inspect(value)}" - end - - defp quoted_binary_encode(binary, meta) do - quote line: meta[:line] do - {:safe, unquote(__MODULE__).binary_encode(unquote(binary))} - end - end - - # We mark attributes as safe so we don't escape them - # at rendering time. However, some attributes are - # specially handled, so we keep them as strings shape. - defp safe_unless_special("aria"), do: :aria - defp safe_unless_special("class"), do: :class - defp safe_unless_special("style"), do: :style - defp safe_unless_special(name), do: {:safe, name} - - @doc false - @impl true - def void?(_), do: false end diff --git a/lib/live_view_native/template/engine.ex b/lib/live_view_native/template/engine.ex new file mode 100644 index 00000000..3a671d99 --- /dev/null +++ b/lib/live_view_native/template/engine.ex @@ -0,0 +1,213 @@ +defmodule LiveViewNative.Template.Engine do + @moduledoc false + + @behaviour Phoenix.Template.Engine + @behaviour Phoenix.LiveView.TagEngine + + @doc false + @impl true + def compile(path, _name) do + quote location: :keep do + require LiveViewNative.Template.Engine + LiveViewNative.Template.Engine.compile(unquote(path)) + end + end + + @doc false + defmacro compile(path) do + trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true) + source = File.read!(path) + + EEx.compile_string(source, + engine: Phoenix.LiveView.TagEngine, + line: 1, + file: path, + trim: trim, + caller: __CALLER__, + source: source, + tag_handler: __MODULE__ + ) + end + + @doc false + @impl true + defdelegate annotate_body(caller), to: Phoenix.LiveView.HTMLEngine + + @doc false + @impl true + defdelegate annotate_caller(file, line), to: Phoenix.LiveView.HTMLEngine + + @doc false + @impl true + def classify_type(":inner_block"), do: {:error, "the slot name :inner_block is reserved"} + def classify_type(":" <> name), do: {:slot, name} + + def classify_type(<> = name) when first in ?A..?Z do + if String.contains?(name, ".") do + {:remote_component, name} + else + {:tag, name} + end + end + + def classify_type("." <> name), + do: {:local_component, name} + + def classify_type(name), do: {:tag, name} + + @doc false + @impl true + def handle_attributes(ast, meta) do + if is_list(ast) and literal_keys?(ast) do + attrs = + Enum.map(ast, fn {key, value} -> + name = to_string(key) + + case handle_attr_escape(name, value, meta) do + :error -> handle_attrs_escape([{safe_unless_special(name), value}], meta) + parts -> {name, parts} + end + end) + + {:attributes, attrs} + else + {:quoted, handle_attrs_escape(ast, meta)} + end + end + + defp handle_attrs_escape(attrs, meta) do + quote line: meta[:line] do + unquote(__MODULE__).attributes_escape(unquote(attrs)) + end + end + + defp handle_attr_escape("class", [head | tail], meta) when is_binary(head) do + {bins, tail} = Enum.split_while(tail, &is_binary/1) + encoded = class_attribute_encode([head | bins]) + + if tail == [] do + [IO.iodata_to_binary(encoded)] + else + tail = + quote line: meta[:line] do + {:safe, unquote(__MODULE__).class_attribute_encode(unquote(tail))} + end + + [IO.iodata_to_binary([encoded, ?\s]), tail] + end + end + + defp handle_attr_escape("class", value, meta) do + [ + quote( + line: meta[:line], + do: {:safe, unquote(__MODULE__).class_attribute_encode(unquote(value))} + ) + ] + end + + defp handle_attr_escape(_name, value, meta) do + case extract_binaries(value, true, [], meta) do + :error -> :error + reversed -> Enum.reverse(reversed) + end + end + + defp literal_keys?([{key, _value} | rest]) when is_atom(key) or is_binary(key), + do: literal_keys?(rest) + + defp literal_keys?([]), do: true + defp literal_keys?(_other), do: false + + defp extract_binaries({:<>, _, [left, right]}, _root?, acc, meta) do + extract_binaries(right, false, extract_binaries(left, false, acc, meta), meta) + end + + defp extract_binaries({:<<>>, _, parts} = binary, _root?, acc, meta) do + Enum.reduce(parts, acc, fn + part, acc when is_binary(part) -> + [binary_encode(part) | acc] + + {:"::", _, [binary, {:binary, _, _}]}, acc -> + [quoted_binary_encode(binary, meta) | acc] + + _, _ -> + throw(:unknown_part) + end) + catch + :unknown_part -> + [quoted_binary_encode(binary, meta) | acc] + end + + defp extract_binaries(binary, _root?, acc, _meta) when is_binary(binary), + do: [binary_encode(binary) | acc] + + defp extract_binaries(value, false, acc, meta), + do: [quoted_binary_encode(value, meta) | acc] + + defp extract_binaries(_value, true, _acc, _meta), + do: :error + + @doc false + def class_attribute_encode(list) when is_list(list), + do: list |> class_attribute_list() |> LiveViewNative.Engine.encode_to_iodata!() + + def class_attribute_encode(other), + do: empty_attribute_encode(other) + + defp class_attribute_list(value) do + value + |> Enum.flat_map(fn + nil -> [] + false -> [] + inner when is_list(inner) -> [class_attribute_list(inner)] + other -> [other] + end) + |> Enum.join(" ") + end + + @doc false + def empty_attribute_encode(nil), do: "" + def empty_attribute_encode(false), do: "" + def empty_attribute_encode(true), do: "" + def empty_attribute_encode(value), do: LiveViewNative.Engine.encode_to_iodata!(value) + + @doc false + def binary_encode(value) when is_binary(value) do + value + |> LiveViewNative.Engine.encode_to_iodata!() + |> IO.iodata_to_binary() + end + + def binary_encode(value) do + raise ArgumentError, "expected a binary in <>, got: #{inspect(value)}" + end + + @doc false + def attributes_escape(attrs) do + attrs + |> Enum.map(fn + {key, value} when is_atom(key) -> {Atom.to_string(key), value} + other -> other + end) + |> LiveViewNative.Template.attributes_escape() + end + + defp quoted_binary_encode(binary, meta) do + quote line: meta[:line] do + {:safe, unquote(__MODULE__).binary_encode(unquote(binary))} + end + end + + # We mark attributes as safe so we don't escape them + # at rendering time. However, some attributes are + # specially handled, so we keep them as strings shape. + defp safe_unless_special("aria"), do: :aria + defp safe_unless_special("class"), do: :class + defp safe_unless_special("style"), do: :style + defp safe_unless_special(name), do: {:safe, name} + + @doc false + @impl true + def void?(_), do: false +end diff --git a/lib/mix/tasks/lvn.setup.config.ex b/lib/mix/tasks/lvn.setup.config.ex index 9d5ec964..5f0e70a1 100644 --- a/lib/mix/tasks/lvn.setup.config.ex +++ b/lib/mix/tasks/lvn.setup.config.ex @@ -163,7 +163,7 @@ defmodule Mix.Tasks.Lvn.Setup.Config do end defp patch_template_engines_data(_context) do - [{:neex, LiveViewNative.Engine}] + [{:neex, LiveViewNative.Template.Engine}] end defp patch_live_reload_patterns_data(context) do diff --git a/test/live_view_native/integrations/lvn_template_test.exs b/test/live_view_native/integrations/lvn_template_test.exs index 4d71f620..1bf7415c 100644 --- a/test/live_view_native/integrations/lvn_template_test.exs +++ b/test/live_view_native/integrations/lvn_template_test.exs @@ -10,7 +10,7 @@ defmodule LiveViewNative.LVNTemplateTest do module: __MODULE__, caller: __CALLER__, source: string, - tag_handler: LiveViewNative.TagEngine + tag_handler: LiveViewNative.Template.Engine ) ) |> Phoenix.HTML.Safe.to_iodata() @@ -27,8 +27,8 @@ defmodule LiveViewNative.LVNTemplateTest do """) =~ "yes" refute compile(""" - yes - """) =~ "yes" + yes + """) =~ "yes" end end end diff --git a/test/live_view_native/tag_engine_test.exs b/test/live_view_native/template/engine_test.exs similarity index 61% rename from test/live_view_native/tag_engine_test.exs rename to test/live_view_native/template/engine_test.exs index da4b726d..6a7cb231 100644 --- a/test/live_view_native/tag_engine_test.exs +++ b/test/live_view_native/template/engine_test.exs @@ -1,39 +1,39 @@ -defmodule LiveViewNative.TagEngineTest do +defmodule LiveViewNative.Template.EngineTest do use ExUnit.Case - alias LiveViewNative.TagEngine + alias LiveViewNative.Template.Engine describe "classify_type/1" do test "it returns a slot tuple when name starts with a colon" do - assert TagEngine.classify_type(":custom_tag") == {:slot, "custom_tag"} + assert Engine.classify_type(":custom_tag") == {:slot, "custom_tag"} end test "it returns an error tuple when name is :inner_block" do - assert TagEngine.classify_type(":inner_block") == + assert Engine.classify_type(":inner_block") == {:error, "the slot name :inner_block is reserved"} end test "it returns a remote component tuple when name is a capitalized string (module name with function)" do - assert TagEngine.classify_type("Foo.Bar.baz") == {:remote_component, "Foo.Bar.baz"} + assert Engine.classify_type("Foo.Bar.baz") == {:remote_component, "Foo.Bar.baz"} end test "it returns a local component tuple when name starts with a period (local function)" do - assert TagEngine.classify_type(".qux") == {:local_component, "qux"} + assert Engine.classify_type(".qux") == {:local_component, "qux"} end test "it returns all other tagas as they are" do - assert TagEngine.classify_type("test") == {:tag, "test"} + assert Engine.classify_type("test") == {:tag, "test"} end test "it returns a tag when name is a capitalized string without a dot" do - assert TagEngine.classify_type("Foo") == {:tag, "Foo"} + assert Engine.classify_type("Foo") == {:tag, "Foo"} end end describe "void?/1" do test "it returns false for all self-closing HTML tags" do for void <- ~w(area base br col hr img input link meta param command keygen source) do - refute TagEngine.void?(void) + refute Engine.void?(void) end end end diff --git a/test/mix/tasks/lvn.setup_test.exs b/test/mix/tasks/lvn.setup_test.exs index c30241dc..7706e508 100644 --- a/test/mix/tasks/lvn.setup_test.exs +++ b/test/mix/tasks/lvn.setup_test.exs @@ -73,7 +73,7 @@ defmodule Mix.Tasks.Lvn.SetupTest do assert file =~ """ config :phoenix, :template_engines, [ - neex: LiveViewNative.Engine + neex: LiveViewNative.Template.Engine ] """ end @@ -201,7 +201,7 @@ defmodule Mix.Tasks.Lvn.SetupTest do assert file =~ """ config :phoenix, :template_engines, [ - neex: LiveViewNative.Engine + neex: LiveViewNative.Template.Engine ] """ end diff --git a/test/support/clients/gameboy.ex b/test/support/clients/gameboy.ex index abc8e552..4a0a8c55 100644 --- a/test/support/clients/gameboy.ex +++ b/test/support/clients/gameboy.ex @@ -3,6 +3,6 @@ defmodule LiveViewNativeTest.GameBoy do format: :gameboy, component: LiveViewNativeTest.GameBoy.Component, module_suffix: :GameBoy, - template_engine: LiveViewNative.Engine, + template_engine: LiveViewNative.Template.Engine, test_client: %LiveViewNativeTest.GameBoy.TestClient{} end diff --git a/test/support/clients/switch.ex b/test/support/clients/switch.ex index 01fa1343..fb54dd94 100644 --- a/test/support/clients/switch.ex +++ b/test/support/clients/switch.ex @@ -3,6 +3,6 @@ defmodule LiveViewNativeTest.Switch do format: :switch, component: LiveViewNativeTest.Switch.Component, module_suffix: :Switch, - template_engine: LiveViewNative.Engine, + template_engine: LiveViewNative.Template.Engine, test_client: %LiveViewNativeTest.Switch.TestClient{} end