diff --git a/CHANGELOG.md b/CHANGELOG.md index add8e2f..5f8f365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,11 @@ I.e. `mjml_nif 0.x` versions use mrml versions `>= 0.1, < 1.0.0`, and `mjml_nif ## [Unreleased] ### Changed -- Use rustler_precompiled v0.7.0 + drop support for Elixir < v1.12 +- Use rustler_precompiled v0.7.1 + drop support for Elixir < v1.12 - Use Rust edition 2021 - Use rustler v0.30.0 - Drop arm-unknown-linux-gnueabihf as precompiled target +- Use [mrml] v.2.1.1 --- diff --git a/README.md b/README.md index 6d6086a..8fd1be9 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,21 @@ Available rendering options are: * `keep_comments` – when `false`, removes comments from the final HTML. Defaults to `true`. * `social_icon_path` – when given, uses this base path to generate social icon URLs. +* `fonts` – a Map of font names and their URLs to a hosted CSS file. + When given, includes these fonts in the rendered HTML + (Note that only actually used fonts will show up!). + Defaults to `nil`, which will make the default font families available to + be used (Open Sans, Droid Sans, Lato, Roboto, and Ubuntu). ```elixir mjml = "..." opts = [ keep_comments: false, - social_icon_path: "https://example.com/icons/" + social_icon_path: "https://example.com/icons/", + fonts: %{ + "Noto Color Emoji": "https://fonts.googleapis.com/css?family=Noto+Color+Emoji:400" + } ] {:ok, html} = Mjml.to_html(mjml, opts) diff --git a/lib/mjml.ex b/lib/mjml.ex index 1af4459..7dca491 100644 --- a/lib/mjml.ex +++ b/lib/mjml.ex @@ -20,6 +20,12 @@ defmodule Mjml do E.g. `social_icon_path: "https://example.com/icons/"` will generate a social icon URL, like "https://example.com/icons/github.png" + * `fonts` – a Map of font names and their URLs to hosted CSS files. + When given, includes these fonts in the rendered HTML + (Note that only actually used fonts will show up!). + Defaults to `nil`, which will make the default font families available to + be used (Open Sans, Droid Sans, Lato, Roboto, and Ubuntu). + ## Examples iex> Mjml.to_html("") @@ -28,7 +34,13 @@ defmodule Mjml do iex> Mjml.to_html("something not MJML") {:error, "Couldn't convert MJML template"} - iex> opts = [keep_comments: false, social_icon_path: "https://example.com/icons/"] + iex> opts = [ + iex> keep_comments: false, + iex> social_icon_path: "https://example.com/icons/" + iex> fonts: %{ + iex> "Noto Color Emoji": "https://fonts.googleapis.com/css?family=Noto+Color+Emoji:400" + iex> } + iex> ] iex> Mjml.to_html("", opts) {:ok, " { pub keep_comments: bool, - pub social_icon_path: Option + pub social_icon_path: Option, + pub fonts: Option, Term<'a>>> } #[rustler::nif] -pub fn to_html<'a>(env: Env<'a>, mjml: String, render_options: Options) -> NifResult> { +pub fn to_html<'a>(env: Env<'a>, mjml: String, render_options: RenderOptions) -> NifResult> { return match mrml::parse(&mjml) { Ok(root) => { - let options = mrml::prelude::render::Options{ + let options = mrml::prelude::render::RenderOptions{ disable_comments: !render_options.keep_comments, - social_icon_origin: render_options.social_icon_path + social_icon_origin: social_icon_origin_option(render_options.social_icon_path), + fonts: fonts_option(render_options.fonts) }; return match root.render(&options) { @@ -33,4 +38,49 @@ pub fn to_html<'a>(env: Env<'a>, mjml: String, render_options: Options) -> NifRe }; } +fn social_icon_origin_option(option_value: Option) -> Option> { + option_value.map_or( + mrml::prelude::render::RenderOptions::default().social_icon_origin, + |origin| Some(Owned(origin)) + ) +} + +fn fonts_option<'a>(option_values: Option, Term<'a>>>) -> HashMap> { + option_values.map_or( + mrml::prelude::render::RenderOptions::default().fonts, + |fonts| -> HashMap> { + let mut options : HashMap> = HashMap::new(); + + for (key, value) in fonts { + let (k, v) = font_option(key, value); + options.insert(k, v); + } + + return options + } + ) +} + +fn font_option<'a>(key: Term<'a>, value: Term<'a>) -> (String, Cow<'static, str>) { + ( + match key.atom_to_string() { + Ok(s) => s, + Err(_) => panic!( + "Keys for the `fonts` render option must be of type Atom, got {:?}. + Please use a Map like this: %{{\"My Font Name\": \"https://myfonts.example.com/css\"}}", + key.get_type() + ) + + }, + match value.decode::() { + Ok(s) => Owned(s), + Err(_) => panic!( + "Values for the `fonts` render option must be of type String, got {:?}. + Please use a Map like this: %{{\"My Font Name\": \"https://myfonts.example.com/css\"}}", + value.get_type() + ) + } + ) +} + rustler::init!("Elixir.Mjml.Native", [to_html]); diff --git a/test/mjml_test.exs b/test/mjml_test.exs index a74d24a..54541af 100644 --- a/test/mjml_test.exs +++ b/test/mjml_test.exs @@ -31,10 +31,10 @@ defmodule MjmlTest do assert String.starts_with?(message, "unexpected element") assert {:error, message} = Mjml.to_html("not MJML") - assert String.starts_with?(message, "parsing error: unknown token") + assert String.starts_with?(message, "unable to load included template") assert {:error, message} = Mjml.to_html("") - assert String.starts_with?(message, "parsing error: invalid element") + assert String.starts_with?(message, "unable to load included template") end describe "when passing options" do @@ -76,5 +76,67 @@ defmodule MjmlTest do assert html =~ "#{social_icon_path}github.png" assert html =~ "#{social_icon_path}twitter.png" end + + test "`fonts: %{\"font name\": \"font URL\", ...}` includes the given fonts" do + fonts = %{ + "My Font": "https://myfontserver.example.com/css?family=My+Font:300,400,500,700", + "Your Font": "https://yourfontserver.example.com/css?family=Your+Font:300,400,500,700" + } + + mjml = """ + + + + + + + + + + + test + + + + + + """ + + assert {:ok, html} = Mjml.to_html(mjml, fonts: fonts) + + [font_name_a, font_name_b] = fonts |> Map.keys() + [font_url_a, font_url_b] = fonts |> Map.values() + + assert html =~ font_name_a |> Atom.to_string() + assert html =~ font_url_a + + assert html =~ font_name_b |> Atom.to_string() + assert html =~ font_url_b + end + + test "not providing `fonts` allows using the default fonts" do + mjml = """ + + + + + + + + + + + test + + + + + + """ + + assert {:ok, html} = Mjml.to_html(mjml) + assert html =~ "Open Sans" + assert html =~ "https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700" + end end end