defmodule Fermo.Compilers.EEx do
import Mix.Fermo.Paths
import Fermo.Naming
defstruct [
:name,
:source,
:frontmatter,
:content_fors,
:template_project_path,
:template_source_path,
:offset
]
@file_impl Application.compile_env(:fermo, :file_impl, File)
def compile(template_project_path) do
template_source_path = absolute_to_source(template_project_path)
name = source_path_to_module(template_source_path)
source = File.read!(template_project_path)
[frontmatter, body] = extract_frontmatter(source)
compile_module(
%__MODULE__{
name: name,
source: body,
frontmatter: frontmatter,
content_fors: [],
template_project_path: template_project_path,
template_source_path: template_source_path,
offset: 0
}
)
end
def extract_frontmatter(source = "---\n" <> _rest) do
[_, frontmatter_yaml, body] = String.split(source, "---\n")
frontmatter = YamlElixir.read_from_string(frontmatter_yaml)
[frontmatter, body]
end
def extract_frontmatter(body) do
[%{}, body]
end
def compile_module(%__MODULE__{} = module) do
# We do a first compilation here so we can trap errors
# and give a better message
try do
EEx.compile_string(module.source, line: module.offset, file: module.template_project_path)
rescue
e in TokenMissingError ->
message = """
Template compilation error: #{e.description}
Path: '#{module.template_project_path}'
"""
raise Fermo.Error, message: message
end
quoted_module = quote(
bind_quoted: [
content_fors: module.content_fors,
frontmatter: Macro.escape(module.frontmatter, unquote: true),
name: module.name,
offset: module.offset,
source: module.source,
template_project_path: module.template_project_path,
template_source_path: module.template_source_path
],
file: module.template_project_path
) do
compiled = EEx.compile_string(source, line: offset, file: template_project_path)
cfs_compiled = Enum.map(content_fors, fn [key, eex, offset] ->
cf_compiled = EEx.compile_string(eex, line: offset, file: template_project_path)
{key, cf_compiled}
end)
defmodule :"#{name}" do
use Helpers
require Fermo.Partial
import Fermo.Partial
require Fermo.YieldContent
import Fermo.YieldContent
require Fermo.Assets
import Fermo.Assets
import Fermo.I18n
Enum.map(cfs_compiled, fn {key, cf_compiled} ->
args = [:"#{key}", Macro.var(:params, nil), Macro.var(:context, nil)]
def content_for(unquote_splicing(args)) do
_params = var!(params)
_context = var!(context)
unquote(cf_compiled)
end
end)
# TODO: Shouldn't this return nil?
# TODO: if we change this to return nil,
# we need to update set_paths/1
def content_for(key, params, context) do
""
end
def template_source_path() do
unquote(template_source_path)
end
# Define a method with the frontmatter, so we can merge with
# params when the template is evaluated
escaped_frontmatter = Macro.escape(frontmatter)
def defaults() do
unquote(escaped_frontmatter)
end
args = [Macro.var(:params, nil), Macro.var(:context, nil)]
def call(unquote_splicing(args)) do
_params = var!(params)
_context = var!(context)
unquote(compiled)
end
end
end
Code.compiler_options(ignore_module_conflict: true)
[{module_name, bytecode} | _other] = Code.compile_quoted(quoted_module)
Code.compiler_options(ignore_module_conflict: false)
base = Mix.Project.compile_path()
module_path = Path.join(base, "#{module_name}.beam")
@file_impl.write!(module_path, bytecode, [:write])
end
end