From 1465105ecd0a7815c25a0159cf5bdeee984a7fea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:08:48 +0000 Subject: [PATCH 1/2] Bump mrml from 1.2.11 to 2.1.1 in /native/mjml_nif Bumps [mrml](https://github.com/jdrouet/mrml) from 1.2.11 to 2.1.1. - [Release notes](https://github.com/jdrouet/mrml/releases) - [Changelog](https://github.com/jdrouet/mrml/blob/main/changelog.md) - [Commits](https://github.com/jdrouet/mrml/compare/v1.2.11...mrml-v2.1.1) --- updated-dependencies: - dependency-name: mrml dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- native/mjml_nif/Cargo.lock | 134 +++++++++++++++++-------------------- native/mjml_nif/Cargo.toml | 2 +- 2 files changed, 62 insertions(+), 74 deletions(-) diff --git a/native/mjml_nif/Cargo.lock b/native/mjml_nif/Cargo.lock index eabb7b2..d7fadfd 100644 --- a/native/mjml_nif/Cargo.lock +++ b/native/mjml_nif/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -12,33 +22,16 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "cfg-if" -version = "1.0.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "getrandom" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -48,11 +41,11 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "indexmap" -version = "1.9.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ - "autocfg", + "equivalent", "hashbrown", "serde", ] @@ -63,12 +56,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "libc" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" - [[package]] name = "memchr" version = "2.4.1" @@ -85,68 +72,55 @@ dependencies = [ [[package]] name = "mrml" -version = "1.2.11" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21c524cad4503cd722fb13286c5199914f2786280733d947d623cbdcb5e6437" +checksum = "13b6cbfe1ae1e7de18c2a51b9709a5f859bfacca91fc1029d8947ae77fe0f652" dependencies = [ "indexmap", - "rand", + "mrml-macros", "rustc-hash", + "thiserror", "xmlparser", ] [[package]] -name = "ppv-lite86" -version = "0.2.16" +name = "mrml-common-macros" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro2" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "c141a78933cb6151e19b1d3daacfd5d2903bc1caf96f6424b6eccc48ca8ce00f" dependencies = [ - "unicode-ident", + "syn", ] [[package]] -name = "quote" -version = "1.0.26" +name = "mrml-macros" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "573a6a8f4b79efc885ff5eaead81be779c8a948792d4bc5aea7979616a2141f2" dependencies = [ + "Inflector", + "mrml-common-macros", "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" +name = "proc-macro2" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ - "ppv-lite86", - "rand_core", + "unicode-ident", ] [[package]] -name = "rand_core" -version = "0.6.3" +name = "quote" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ - "getrandom", + "proc-macro2", ] [[package]] @@ -222,6 +196,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.8" @@ -243,12 +237,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "xmlparser" version = "0.13.3" diff --git a/native/mjml_nif/Cargo.toml b/native/mjml_nif/Cargo.toml index 64d3975..fd8581c 100644 --- a/native/mjml_nif/Cargo.toml +++ b/native/mjml_nif/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] rustler = "0.30.0" -mrml = { version = "1.2", default-features = false, features = ["parse", "render", "orderedmap"] } +mrml = { version = "2.1", default-features = false, features = ["parse", "render", "orderedmap"] } [features] default = ["nif_version_2_15"] From fad656f5203eaaf3511ee639129e28854fc4ef53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20G=C3=B6tze?= Date: Sun, 10 Dec 2023 17:52:09 +0100 Subject: [PATCH 2/2] Implement fonts render option in order to allow using custom fonts in the email template. the `fonts` option must be provided as a map of the font name and its URL of a hosted CSS file: e.g. fonts: %{"Noto Color Emoji": "https://fonts.googleapis.com/css?family=Noto+Color+Emoji:400"} --- CHANGELOG.md | 3 +- README.md | 10 +++++- lib/mjml.ex | 14 +++++++- lib/mjml/render_options.ex | 2 +- native/mjml_nif/src/lib.rs | 60 +++++++++++++++++++++++++++++++--- test/mjml_test.exs | 66 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 144 insertions(+), 11 deletions(-) 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