Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce LiveViewNative.Template.Engine #225

Merged
merged 1 commit into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions lib/live_view_native/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 9 additions & 20 deletions lib/live_view_native/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
183 changes: 0 additions & 183 deletions lib/live_view_native/tag_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule LiveViewNative.TagEngine do
"""

alias Phoenix.LiveView.Tokenizer
@behaviour Phoenix.LiveView.TagEngine
@behaviour EEx.Engine

@impl true
Expand Down Expand Up @@ -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(<<first, _::binary>> = 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
Loading