From fa00555677bacf52388a5b3799234231af970bb8 Mon Sep 17 00:00:00 2001 From: Stephen Moloney Date: Tue, 11 Oct 2016 00:21:49 +0100 Subject: [PATCH 1/4] make changes to allow for multiple smoothie configurations --- README.md | 18 +++++-- lib/mix/tasks/compile.ex | 13 ++++- lib/smoothie.ex | 104 ++++++++++++++++++--------------------- mix.exs | 3 +- 4 files changed, 76 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 6244b38..caa29b4 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,19 @@ p, ul, ol { Now create the template for our email, we can save this in `web/mailers/templates/welcome.html.eex` +Optionally adding additional css styling specific for this template partial is possible using `` tags. + ```html + +

Hi <%= name %>,

-

Welcome!

+

Welcome!

Cheers,

@@ -231,7 +240,7 @@ defmodule MyApp.Mailer do # your mailgun config here @config %{...} use Mailgun.Client, @config - use Smoothie + use Smoothie, otp_app: MyApp, config: MyApp.Newsletter.Smoothie def welcome_email(user) do template_params = [ @@ -285,12 +294,13 @@ Smoothie can be installed as: 3. Specify the locations of your templates, edit `config/confix.exs` in your Elixir project and add the following config: ```elixir - config :smoothie, template_dir: Path.join(["web", "mailers", "templates"]) + config :my_app, MyApp.Newsletter.Smoothie, + template_dir: Path.join(["web", "mailers", "templates"]) ``` It can also be in any other directory, just provide the correct directory here. - It is really important to make sure this directory exists, otherwise your project will not compile. + Additional layouts can be used by adding additional configurations. 4. The only thing left is install the npm package that smoothie relies on in your project, we can do this with the following command: diff --git a/lib/mix/tasks/compile.ex b/lib/mix/tasks/compile.ex index fcdd9d3..10acd5e 100644 --- a/lib/mix/tasks/compile.ex +++ b/lib/mix/tasks/compile.ex @@ -3,6 +3,17 @@ defmodule Mix.Tasks.Smoothie.Compile do @shortdoc "Compiles smoothie templates" def run(_) do - System.cmd Path.join([File.cwd!, "node_modules/.bin/elixir-smoothie"]), [], env: [{"MAIL_TEMPLATE_DIR", Application.get_env(:smoothie, :template_dir)}], into: IO.stream(:stdio, :line) + otp_app = Mix.Project.config[:app] + configs = Application.get_env(otp_app, :smoothie_configs) + + for config <- configs do + path = Path.join([File.cwd!, "node_modules/.bin/elixir-smoothie"]) + env = [ + {"MAIL_TEMPLATE_DIR", Application.get_env(otp_app, config)[:template_dir]}, + {"LAYOUT_TEMPLATE_DIR", Application.get_env(otp_app, config)[:layout_dir]} + ] + System.cmd(path, [], env: env, into: IO.stream(:stdio, :line)) + end + end end diff --git a/lib/smoothie.ex b/lib/smoothie.ex index 965a9dc..7ff776d 100644 --- a/lib/smoothie.ex +++ b/lib/smoothie.ex @@ -1,71 +1,63 @@ defmodule Smoothie do require EEx - defmacro __using__(_options) do - quote do - import unquote(__MODULE__) + defmacro __using__(opts) do - generate_views() - end - end - - # location of the template path - @template_path [Mix.Project.build_path, '..', '..'] ++ [Application.get_env(:smoothie, :template_dir)] - - # location of the build path - @build_path @template_path ++ ["build"] - - # create the template and build folder at compile time if not exists - unless File.exists?(Path.join(@build_path)), do: File.mkdir_p!(Path.join(@build_path)) + quote bind_quoted: [opts: opts] do - @template_files File.ls!(Path.join(@build_path)) - |> Enum.filter(fn(file) -> String.contains?(file, ".eex") end) - - # Ensure the macro is recompiled when the templates are changed - @template_files - |> Enum.each(fn(file) -> - @external_resource Path.join(@build_path ++ [file]) - end) - - defmacro generate_views do - @template_files - |> Enum.map(fn(file) -> - # read the contents of the template - template_contents = File.read!(Path.join(@build_path ++ [file])) - - # capture variables that are defined in the template - variables = - Regex.scan(~r/<%=[^\w]*(\w+)[^\w]*%>/, template_contents) - |> Enum.map(fn(match) -> - match - |> Enum.at(1) - |> String.to_atom - end) - |> Enum.uniq + otp_app = Keyword.fetch!(opts, :otp_app) + smoothie_config = Keyword.fetch!(opts, :config) + template_path = [Mix.Project.build_path, '..', '..'] ++ [Application.get_env(otp_app, smoothie_config)[:template_dir]] + build_path = template_path ++ ["build"] + template_files = File.ls!(Path.join(build_path)) - # create assignment macro code for in the function block - variable_assignments = variables |> Enum.map(fn(name) -> - quote do - unquote(Macro.var(name, nil)) = args[unquote(name)] - end + # Ensure the macro is recompiled when the templates are changed + Enum.each(template_files, fn(file) -> + external_resource = Path.join(build_path ++ [file]) end) - # generate function name from file name - template_name = - file - |> String.replace(".eex", "") - |> String.replace(".html", "_html") - |> String.replace(".txt", "_text") - |> String.to_atom + Enum.each(template_files, fn(file) -> + # read the contents of the template + template_contents = File.read!(Path.join(build_path ++ [file])) + + # capture variables that are defined in the template + variables = + Regex.scan(~r/<%=(.*?)%>/, template_contents) + |> Enum.map(fn(match) -> + match + |> Enum.at(1) + |> String.trim(" ") + |> String.to_atom() + end) + |> Enum.uniq + + # create assignment macro code for in the function block + variable_assignments = quote do: (unquote(variables) + |> Enum.map(fn(name) -> + quote do + unquote(Macro.var(name, nil)) = args[unquote(name)] + end + end) + ) + + # generate function name from file name + template_name = + file + |> String.replace(".eex", "") + |> String.replace(".html", "_html") + |> String.replace(".txt", "_text") + |> String.to_atom - compiled = EEx.compile_string(template_contents, []) + compiled = quote do: EEx.compile_string(unquote(template_contents), []) - quote do def unquote(template_name)(args) do unquote(variable_assignments) unquote(compiled) end - end - end) + end) + + end end -end + + +end \ No newline at end of file diff --git a/mix.exs b/mix.exs index 38e0862..4d381cf 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,8 @@ defmodule Smoothie.Mixfile do [ applications: [:logger], env: [ - template_dir: Path.join(["web", "mailers", "templates"]) + template_dir: Path.join(["web", "mailers", "templates"]), + layout_dir: Path.join(["web", "mailers", "templates", "layout"]) ] ] end From 2e5cd78b9c7086bc3ba817ecc8d631bcec3c7a0a Mon Sep 17 00:00:00 2001 From: Stephen Moloney Date: Tue, 11 Oct 2016 12:38:17 +0100 Subject: [PATCH 2/4] fixed an issue with the function generation and macros --- lib/smoothie.ex | 65 +++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/lib/smoothie.ex b/lib/smoothie.ex index 7ff776d..6c47353 100644 --- a/lib/smoothie.ex +++ b/lib/smoothie.ex @@ -5,57 +5,54 @@ defmodule Smoothie do quote bind_quoted: [opts: opts] do - otp_app = Keyword.fetch!(opts, :otp_app) - smoothie_config = Keyword.fetch!(opts, :config) - template_path = [Mix.Project.build_path, '..', '..'] ++ [Application.get_env(otp_app, smoothie_config)[:template_dir]] - build_path = template_path ++ ["build"] - template_files = File.ls!(Path.join(build_path)) + @otp_app Keyword.fetch!(opts, :otp_app) + @smoothie_config Keyword.fetch!(opts, :config) + @template_path [Mix.Project.build_path(), '..', '..'] ++ [Application.get_env(@otp_app, @smoothie_config)[:template_dir]] + @build_path @template_path ++ ["build"] + @template_files File.ls!(Path.join(@build_path)) # Ensure the macro is recompiled when the templates are changed - Enum.each(template_files, fn(file) -> - external_resource = Path.join(build_path ++ [file]) + Enum.each(@template_files, fn(file) -> + @external_resource Path.join(@build_path ++ [file]) end) - Enum.each(template_files, fn(file) -> + Enum.each(@template_files, fn(file) -> # read the contents of the template - template_contents = File.read!(Path.join(build_path ++ [file])) + @template_contents File.read!(Path.join(@build_path ++ [file])) + |> Og.log_return(__ENV__, :debug) # capture variables that are defined in the template - variables = - Regex.scan(~r/<%=(.*?)%>/, template_contents) - |> Enum.map(fn(match) -> - match - |> Enum.at(1) - |> String.trim(" ") - |> String.to_atom() - end) - |> Enum.uniq + @variables Regex.scan(~r/<%=(.*?)%>/, @template_contents) + |> Enum.map(fn(match) -> + match + |> Enum.at(1) + |> String.trim(" ") + |> String.to_atom() + end) + |> Enum.uniq + |> Og.log_return(__ENV__, :debug) # create assignment macro code for in the function block - variable_assignments = quote do: (unquote(variables) - |> Enum.map(fn(name) -> + @variable_assignments Enum.map(@variables, fn(name) -> quote do unquote(Macro.var(name, nil)) = args[unquote(name)] end end) - ) # generate function name from file name - template_name = - file - |> String.replace(".eex", "") - |> String.replace(".html", "_html") - |> String.replace(".txt", "_text") - |> String.to_atom - - compiled = quote do: EEx.compile_string(unquote(template_contents), []) - - def unquote(template_name)(args) do - unquote(variable_assignments) - unquote(compiled) + @template_name file + |> String.replace(".eex", "") + |> String.replace(".html", "_html") + |> String.replace(".txt", "_text") + |> String.to_atom + + @compiled EEx.compile_string(@template_contents, []) + + def unquote(@template_name)(args) do + unquote(@variable_assignments) + unquote(@compiled) end end) - end end From afd5112aa977f19880c572553fc45adcb49058e3 Mon Sep 17 00:00:00 2001 From: Stephen Moloney Date: Tue, 11 Oct 2016 15:07:25 +0100 Subject: [PATCH 3/4] remove logger calls --- lib/smoothie.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/smoothie.ex b/lib/smoothie.ex index 6c47353..1b6aa8b 100644 --- a/lib/smoothie.ex +++ b/lib/smoothie.ex @@ -19,7 +19,6 @@ defmodule Smoothie do Enum.each(@template_files, fn(file) -> # read the contents of the template @template_contents File.read!(Path.join(@build_path ++ [file])) - |> Og.log_return(__ENV__, :debug) # capture variables that are defined in the template @variables Regex.scan(~r/<%=(.*?)%>/, @template_contents) @@ -30,7 +29,6 @@ defmodule Smoothie do |> String.to_atom() end) |> Enum.uniq - |> Og.log_return(__ENV__, :debug) # create assignment macro code for in the function block @variable_assignments Enum.map(@variables, fn(name) -> From a8608a3dc6a52ef1074f9a8036b641ff6570f3c6 Mon Sep 17 00:00:00 2001 From: Stephen Moloney Date: Tue, 11 Oct 2016 18:27:19 +0100 Subject: [PATCH 4/4] improve docs and add options for no-layout and foundation emails --- README.md | 31 +++++++++++++++++++++++++++---- lib/mix/tasks/compile.ex | 14 ++++++++++++-- lib/smoothie.ex | 1 + 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index caa29b4..b1dcf1f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Inline styling and plain text template generation for your email templates. Follow the installation instructions to set up Smoothie. After that we can use it in the following way in our project: -Let's suppose we are using the excellent Mailgun library to send our emails. Then we set up a Mailer module in the following location: `web/mailers/mailer.ex`, with the following content: +Let's suppose we are using the excellent Mailgun library to send our emails. +Then we set up a Mailer module in the following location: `web/mailers/mailer.ex`, with the following content: ```elixir defmodule MyApp.Mailer do @@ -240,7 +241,7 @@ defmodule MyApp.Mailer do # your mailgun config here @config %{...} use Mailgun.Client, @config - use Smoothie, otp_app: MyApp, config: MyApp.Newsletter.Smoothie + use Smoothie, otp_app: MyApp, config: __MODULE__ def welcome_email(user) do template_params = [ @@ -294,8 +295,15 @@ Smoothie can be installed as: 3. Specify the locations of your templates, edit `config/confix.exs` in your Elixir project and add the following config: ```elixir - config :my_app, MyApp.Newsletter.Smoothie, - template_dir: Path.join(["web", "mailers", "templates"]) + config :my_app, MyApp.Mailer, + template_dir: Path.join(["web", "mailers", "templates"]), + layout_dir: Path.join(["web", "mailers", "shared_layout"]) + + config :my_app, MyApp.Mailer.Newsletter, + template_dir: Path.join(["web", "mailers", "newsletters", "templates"]), + layout_dir: Path.join(["web", "mailers", "newsletters", "templates", "layout"]) + + config :my_app, smoothie_configs: [MyApp.Mailer, MyApp.Mailer.Newsletter] ``` It can also be in any other directory, just provide the correct directory here. @@ -308,6 +316,21 @@ Smoothie can be installed as: > mix smoothie.init ``` + Compile with layout + ``` + > mix smoothie.compile + ``` + + Compile without a layout + ``` + > mix smoothie.compile --no-layout + ``` + + Compile [foundation-emails](https://github.com/zurb/foundation-emails) [inky](https://github.com/zurb/inky) templating format + ``` + > mix smoothie.compile --no-layout --foundation + ``` + if you want to do it manually, that's also possible use: ``` diff --git a/lib/mix/tasks/compile.ex b/lib/mix/tasks/compile.ex index 10acd5e..201b6df 100644 --- a/lib/mix/tasks/compile.ex +++ b/lib/mix/tasks/compile.ex @@ -2,7 +2,15 @@ defmodule Mix.Tasks.Smoothie.Compile do use Mix.Task @shortdoc "Compiles smoothie templates" - def run(_) do + def run(opts) do + + opts = OptionParser.parse(opts) + |> Tuple.to_list() + |> List.first() + + use_foundation = opts[:foundation] || :false + use_layout = if opts[:no_layout] == :true, do: :false, else: :true + otp_app = Mix.Project.config[:app] configs = Application.get_env(otp_app, :smoothie_configs) @@ -10,7 +18,9 @@ defmodule Mix.Tasks.Smoothie.Compile do path = Path.join([File.cwd!, "node_modules/.bin/elixir-smoothie"]) env = [ {"MAIL_TEMPLATE_DIR", Application.get_env(otp_app, config)[:template_dir]}, - {"LAYOUT_TEMPLATE_DIR", Application.get_env(otp_app, config)[:layout_dir]} + {"LAYOUT_TEMPLATE_DIR", Application.get_env(otp_app, config)[:layout_dir]}, + {"USE_FOUNDATION_EMAILS", Atom.to_string(use_foundation)}, + {"USE_LAYOUT", Atom.to_string(use_layout)} ] System.cmd(path, [], env: env, into: IO.stream(:stdio, :line)) end diff --git a/lib/smoothie.ex b/lib/smoothie.ex index 1b6aa8b..55e2541 100644 --- a/lib/smoothie.ex +++ b/lib/smoothie.ex @@ -10,6 +10,7 @@ defmodule Smoothie do @template_path [Mix.Project.build_path(), '..', '..'] ++ [Application.get_env(@otp_app, @smoothie_config)[:template_dir]] @build_path @template_path ++ ["build"] @template_files File.ls!(Path.join(@build_path)) + unless File.exists?(Path.join(@build_path)), do: File.mkdir_p!(Path.join(@build_path)) # Ensure the macro is recompiled when the templates are changed Enum.each(@template_files, fn(file) ->