Skip to content

Commit

Permalink
Fix CSS compiler
Browse files Browse the repository at this point in the history
Execute template post processing lifecycle on app boot

Close #392
  • Loading branch information
leandrocp committed Jan 29, 2024
1 parent 4d29550 commit 3c06e8f
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 77 deletions.
25 changes: 6 additions & 19 deletions lib/beacon/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule Beacon.Config do
{identifier :: atom(),
fun ::
(template :: String.t(), Beacon.Template.LoadMetadata.t() ->
{:cont, String.t() | Beacon.Template.t()} | {:halt, String.t() | Beacon.Template.t()} | {:halt, Exception.t()})}
{:cont, String.t()} | {:halt, String.t()} | {:halt, Exception.t()})}
]}
]}
| {:render_template,
Expand Down Expand Up @@ -154,16 +154,10 @@ defmodule Beacon.Config do
}

@default_load_template [
{:heex,
[
safe_code_check: &Beacon.Template.HEEx.safe_code_check/2,
compile_heex: &Beacon.Template.HEEx.compile/2
]},
{:heex, []},
{:markdown,
[
convert_to_html: &Beacon.Template.Markdown.convert_to_html/2,
safe_code_check: &Beacon.Template.HEEx.safe_code_check/2,
compile_heex: &Beacon.Template.HEEx.compile/2
convert_to_html: &Beacon.Template.Markdown.convert_to_html/2
]}
]

Expand Down Expand Up @@ -277,8 +271,7 @@ defmodule Beacon.Config do
load_template: [
{:custom_format,
[
validate: fn template, _metadata -> MyEngine.validate(template) end,
build_rendered: fn template, _metadata -> %Phoenix.LiveView.Rendered{static: template} end,
validate: fn template, _metadata -> MyEngine.validate(template) end
]}
],
render_template: [
Expand Down Expand Up @@ -312,18 +305,12 @@ defmodule Beacon.Config do
]
lifecycle: [
load_template: [
heex: [
safe_code_check: &Beacon.Template.HEEx.safe_code_check/2,
compile_heex: &Beacon.Template.HEEx.compile/2
],
heex: [],
markdown: [
convert_to_html: &Beacon.Template.Markdown.convert_to_html/2,
safe_code_check: &Beacon.Template.HEEx.safe_code_check/2,
compile_heex: &Beacon.Template.HEEx.compile/2
],
custom_format: [
validate: #Function<41.3316493/2 in :erl_eval.expr/6>,
build_rendered: #Function<41.3316494/2 in :erl_eval.expr/6>
validate: #Function<41.3316493/2 in :erl_eval.expr/6>
]
],
render_template: [
Expand Down
8 changes: 4 additions & 4 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2072,10 +2072,10 @@ defmodule Beacon.Content do

defp do_validate_template(changeset, field, :heex = _format, template, metadata) when is_binary(template) do
Changeset.validate_change(changeset, field, fn ^field, template ->
case Beacon.Template.HEEx.compile(template, metadata) do
{:cont, _ast} -> []
{:halt, %{description: description}} -> [{field, {"invalid", compilation_error: description}}]
{:halt, _} -> [{field, "invalid"}]
case Beacon.Template.HEEx.compile(metadata.site, metadata.path, template) do
{:ok, _ast} -> []
{:error, %{description: description}} -> [{field, {"invalid", compilation_error: description}}]
{:error, _} -> [{field, "invalid"}]
end
end)
end
Expand Down
7 changes: 4 additions & 3 deletions lib/beacon/lifecycle/template.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ defmodule Beacon.Lifecycle.Template do
lifecycle
else
raise Beacon.LoaderError, """
For site: #{site_config.site}
failed to validate lifecycle input for site: #{site_config.site}
#{format_allowed_error_text(format_allowed?, sub_key, allowed_formats)}
#{unconfigured_error_text(format_configured?, sub_key)}
Expand Down Expand Up @@ -81,9 +82,9 @@ defmodule Beacon.Lifecycle.Template do

@doc """
Load a `page` template using the registered format used on the `page`.
This stage runs after fetching the page from the database and before storing the template into ETS.
This stage runs after fetching the page from the database and before compiling and storing the template into ETS.
"""
@spec load_template(Beacon.Content.Page.t()) :: Beacon.Template.t()
@spec load_template(Beacon.Content.Page.t()) :: String.t()
def load_template(page) do
lifecycle = Lifecycle.execute(__MODULE__, page.site, :load_template, page.template, sub_key: page.format, context: %{path: page.path})
lifecycle.output
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/loader/component_module_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Beacon.Loader.ComponentModuleLoader do

defp render_component(%Content.Component{site: site, name: name, body: body}) do
file = "site-#{site}-component-#{name}"
ast = Beacon.Template.HEEx.compile_heex_template!(file, body)
{:ok, ast} = Beacon.Template.HEEx.compile(site, "", body, file)

quote do
def render(unquote(name), var!(assigns)) when is_map(var!(assigns)) do
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/loader/layout_module_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule Beacon.Loader.LayoutModuleLoader do

defp render_layout(layout) do
file = "site-#{layout.site}-layout-#{layout.title}"
ast = Beacon.Template.HEEx.compile_heex_template!(file, layout.template)
{:ok, ast} = Beacon.Template.HEEx.compile(layout.site, "", layout.template, file)

quote do
def render(var!(assigns)) when is_map(var!(assigns)) do
Expand Down
11 changes: 9 additions & 2 deletions lib/beacon/loader/page_module_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Beacon.Loader.PageModuleLoader do
alias Beacon.Content
alias Beacon.Lifecycle
alias Beacon.Loader
alias Beacon.Template.HEEx

require Logger

Expand Down Expand Up @@ -208,7 +209,9 @@ defmodule Beacon.Loader.PageModuleLoader do
end

defp render(page, :request) do
primary = Lifecycle.Template.load_template(page)
primary_template = Lifecycle.Template.load_template(page)
{:ok, primary} = HEEx.compile(page.site, page.path, primary_template)

variants = load_variants(page)

case variants do
Expand Down Expand Up @@ -248,10 +251,14 @@ defmodule Beacon.Loader.PageModuleLoader do
%{variants: variants} = Beacon.Repo.preload(page, :variants)

for variant <- variants do
page = %{page | template: variant.template}
template = Lifecycle.Template.load_template(page)
{:ok, page} = HEEx.compile(page.site, page.path, template)

[
variant.name,
variant.weight,
Lifecycle.Template.load_template(%{page | template: variant.template})
page
]
end
end
Expand Down
8 changes: 0 additions & 8 deletions lib/beacon/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,6 @@ defmodule Beacon.Router do
|> List.flatten()
end

def dump_page_modules(site, fun \\ &Function.identity/1) do
site
|> dump_pages()
|> Enum.map(fn {{^site, _path} = key, {_page_id, _layout_id, _format, page_module, _component_module}} ->
fun.(Tuple.append(key, page_module))
end)
end

@doc false
def lookup_path(site, path) do
lookup_path(@ets_table, site, path)
Expand Down
20 changes: 5 additions & 15 deletions lib/beacon/tailwind_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,10 @@ defmodule Beacon.TailwindCompiler do
end)
end),
Task.async(fn ->
# parse from laoded pages (ETS) so it can fetch callback transformations
# thay may include additoinal stylesheet classes as the markdown parser does
Beacon.Router.dump_page_modules(site, fn {_site, path, page_module} ->
template =
page_module
|> Beacon.Template.render()
|> fetch_static()
|> List.to_string()

page_path = Path.join(tmp_dir, "#{site}_page_#{remove_special_chars(path)}.template")
File.write!(page_path, template)
Enum.map(Content.list_published_pages(site), fn page ->
page_path = Path.join(tmp_dir, "#{site}_page_#{remove_special_chars(page.path)}.template")
post_processed_template = Beacon.Lifecycle.Template.load_template(page)
File.write!(page_path, post_processed_template)
page_path
end)
end),
Expand All @@ -215,9 +208,6 @@ defmodule Beacon.TailwindCompiler do
|> List.flatten()
end

defp fetch_static(%{static: static}), do: static
defp fetch_static(_), do: []

# import app css into input css used by tailwind-cli to load tailwind functions and directives
defp generate_input_css_file!(tmp_dir) do
content = ~S|
Expand Down Expand Up @@ -246,7 +236,7 @@ defmodule Beacon.TailwindCompiler do
input_css_path
end

defp remove_special_chars(name), do: String.replace(name, ~r/[^[:alnum:]_-]+/, "_")
defp remove_special_chars(name), do: String.replace(name, ~r/[^[:alnum:]_]+/, "_")

# include paths for the following scenarios:
# - regular app
Expand Down
33 changes: 13 additions & 20 deletions lib/beacon/template/heex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,35 @@ defmodule Beacon.Template.HEEx do
Handle loading and compilation of HEEx templates.
"""

require Logger

@doc """
Check if the template is safe.
Perform the check using https://github.com/TheFirstAvenger/safe_code
"""
@spec safe_code_check(Beacon.Template.t(), Beacon.Template.LoadMetadata.t()) :: {:cont, Beacon.Template.t()} | {:halt, Exception.t()}
def safe_code_check(template, _metadata) when is_binary(template) do
@spec safe_code_check(String.t()) :: {:ok, String.t()} | {:error, Exception.t()}
def safe_code_check(template) when is_binary(template) do
# TODO: enable safe code when it's ready to parse complex templates
# SafeCode.Validator.validate!(template, extra_function_validators: Beacon.Loader.SafeCodeImpl)
{:cont, template}
{:ok, template}
rescue
exception ->
{:halt, exception}
{:error, exception}
end

@doc """
Compile `template` returning its AST.
"""
@spec compile(Beacon.Template.t(), Beacon.Template.LoadMetadata.t()) :: {:cont, Beacon.Template.ast()} | {:halt, Exception.t()}
def compile(template, metadata) when is_binary(template) do
file = "site-#{metadata.site}-page-#{metadata.path}"
ast = compile_heex_template!(file, template)
# :cont so others can reuse this step
{:cont, ast}
@spec compile(Beacon.Types.Site.t(), String.t(), String.t()) :: {:ok, Beacon.Template.ast()} | {:error, Exception.t()}
def compile(site, path, template, file \\ nil) when is_atom(site) and is_binary(path) and is_binary(template) do
file = if file, do: file, else: "site-#{site}-path-#{path}"
ast = compile_template!(file, template)
{:ok, ast}
rescue
exception ->
{:halt, exception}
{:error, exception}
end

@doc false
def compile_heex_template!(file, template) do
defp compile_template!(file, template) do
opts =
[
engine: Phoenix.LiveView.TagEngine,
Expand Down Expand Up @@ -80,11 +76,8 @@ defmodule Beacon.Template.HEEx do
]

env = %{env | functions: functions}

{rendered, _} =
"nofile"
|> compile_heex_template!(template)
|> Code.eval_quoted([assigns: assigns], env)
{:ok, ast} = compile(site, "", template)
{rendered, _} = Code.eval_quoted(ast, [assigns: assigns], env)

rendered
|> Phoenix.HTML.Safe.to_iodata()
Expand Down
20 changes: 19 additions & 1 deletion test/beacon/tailwind_compiler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,20 @@ defmodule Beacon.TailwindCompilerTest do

published_page_fixture(
layout_id: layout.id,
path: "/a",
path: "/tailwind-test",
template: """
<main>
<h2 class="text-gray-200">Some Values:</h2>
<%= for val <- @beacon_live_data[:vals] do %>
<%= my_component("sample_component", val: val) %>
<% end %>
</main>
"""
)

published_page_fixture(
layout_id: layout.id,
path: "/tailwind-test-post-process",
template: """
<main>
<h2 class="text-gray-200">Some Values:</h2>
Expand Down Expand Up @@ -88,6 +101,11 @@ defmodule Beacon.TailwindCompilerTest do
refute output =~ "text-gray-300"
end)
end

test "fetch post processed page templates" do
assert {:ok, output} = TailwindCompiler.compile(@site)
assert output =~ "text-blue-200"
end
end

describe "compile template" do
Expand Down
21 changes: 18 additions & 3 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ Supervisor.start_link(
tailwind_config: Path.join([File.cwd!(), "test", "support", "tailwind.config.templates.js.eex"]),
data_source: Beacon.BeaconTest.BeaconDataSource,
live_socket_path: "/custom_live",
extra_page_fields: [Beacon.BeaconTest.PageFields.TagsField]
extra_page_fields: [Beacon.BeaconTest.PageFields.TagsField],
lifecycle: [
load_template: [
{:heex,
[
tailwind_test: fn
template, %{site: :my_site, path: "/tailwind-test-post-process"} ->
template = String.replace(template, "text-gray-200", "text-blue-200")
{:cont, template}

template, _metadata ->
{:cont, template}
end
]}
]
]
],
[
site: :s3_site,
Expand Down Expand Up @@ -58,8 +73,8 @@ Supervisor.start_link(
[
div_to_p: fn template, _metadata -> {:cont, String.replace(template, "div", "p")} end,
assigns: fn template, _metadata -> {:cont, String.replace(template, "{ title }", "Beacon")} end,
compile: fn template, _metadata ->
ast = Beacon.Template.HEEx.compile_heex_template!("nofile", template)
compile: fn template, metadata ->
{:ok, ast} = Beacon.Template.HEEx.compile(metadata.site, metadata.path, template)
{:cont, ast}
end,
eval: fn template, _metadata ->
Expand Down

0 comments on commit 3c06e8f

Please sign in to comment.