diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7512d1a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,323 @@ +# Contributing + +Thanks so much for wanting to help! We really appreciate it. + +* Have an idea for a new feature? +* Want to add a new built-in theme? + +Excellent! You've come to the right place. + +1. If you find a bug or wish to suggest a new feature, please create an issue first +2. Make sure your code & comment conventions are in-line with the project's style (execute gometalinter as in [.travis.yml](.travis.yml) file) +3. Make your commits and PRs as tiny as possible - one feature or bugfix at a time +4. Write detailed commit messages, in-line with the project's commit naming conventions + +## Theming Instructions + +This file contains instructions on adding themes to Mailgen: + +* [Using a Custom Theme](#using-a-custom-theme) +* [Creating a Built-In Theme](#creating-a-built-in-theme) + +> We use Golang templates under the hood to inject the e-mail body into themes. + +### Using a Custom Theme + +If you want to supply your own **custom theme** for Hermes to use (but don't want it included with Mailgen): + +1. Create a new struct implementing `Theme` interface ([hermes.go](hermes.go)). An real-life example is in [default.go](default.go) +2. Supply your new theme at hermes creation + +```go + +type MyCustomTheme struct{} + +func (dt *MyCustomTheme) Name() string { + return "mycustomthem" +} + +func (dt *MyCustomTheme) HTMLTemplate() string { + // Get the template from a file (if you want to be able to change the template live without retstarting your application) + // Or write the template by returning pure string here (if you want embbeded template and do not bother with external dependencies) + return "" +} + +func (dt *Default) PlainTextTemplate() string { + // Get the template from a file (if you want to be able to change the template live without retstarting your application) + // Or write the template by returning pure string here (if you want embbeded template and do not bother with external dependencies) + return "" +} + +h := hermes.Hermes{ + Theme: new(MyCustomTheme) // Set your fresh new theme here + Product: hermes.Product{ + Name: "Hermes", + Link: "https://example-hermes.com/", + }, +} + +// ... +// Continue with the rest as usual, create your email and generate the content. +// ... +``` + +3. That's it. + +### Creating a Built-In Theme + +If you want to create a new **built-in** Mailgen theme: + +1. Fork the repository to your GitHub account and clone it to your computer +2. Create a new Go file named after your new theme +3. Copy content of [default.go](default.go) file in new file and make any necessary changes +6. Scroll down to the [injection snippets](#injection-snippets) and copy and paste each code snippet into the relevant area of your template markup +7. Test the theme by running the `examples/*.js` scripts (insert your theme name in each script) and observing the template output in `preview.html` +8. Take a screenshot of your theme portraying each example and place it in `screenshots/{theme}/{example}.png` +9. Add the theme name, credit, and screenshots to the `README.md` file's [Supported Themes](README.md#supported-themes) section (copy one of the existing themes' markup and modify it accordingly) +7. Submit a pull request with your changes and we'll let you know if anything's missing! + +Thanks again for your contribution! + +# Injection Snippets + +## Product Branding Injection + +The following will inject either the product logo or name into the template. + +```html + + <% if (locals.product.logo) { %> + + <% } else { %> + <%- product.name %> + <% } %> + +``` + +It's a good idea to add the following CSS declaration to set `max-height: 50px` for the logo: + +```css +.email-logo { + max-height: 50px; +} +``` + +## Title Injection + +The following will inject the e-mail title (Hi John Appleseed,) or a custom title provided by the user: + +```html +<%- title %> +``` + +## Intro Injection + +The following will inject the intro text (string or array) into the e-mail: + +```html +<% if (locals.intro) { %> + <% intro.forEach(function (introItem) { -%> +

<%- introItem %>

+ <% }) -%> +<% } %> +``` + +## Dictionary Injection + +The following will inject a `
` of key-value pairs into the e-mail: + +```html + +<% if (locals.dictionary) { %> +
+ <% for (item in dictionary) { -%> +
<%- item.charAt(0).toUpperCase() + item.slice(1) %>:
+
<%- dictionary[item] %>
+ <% } -%> +
+<% } %> +``` + +It's a good idea to add this to the top of the template to improve the styling of the dictionary: + +```css +/* Dictionary */ +.dictionary { + width: 100%; + overflow: hidden; + margin: 0 auto; + padding: 0; +} +.dictionary dt { + clear: both; + color: #000; + font-weight: bold; + margin-right: 4px; +} +.dictionary dd { + margin: 0 0 10px 0; +} +``` + +## Table Injection + +The following will inject the table into the e-mail: + +```html + +<% if (locals.table) { %> + + + <% for (var column in table.data[0]) {%> + + <% } %> + + <% for (var i in table.data) {%> + + <% for (var column in table.data[i]) {%> + + <% } %> + + <% } %> +
+ width="<%= table.columns.customWidth[column] %>" + <% } %> + <% if(locals.table.columns && locals.table.columns.customAlignment && locals.table.columns.customAlignment[column]) { %> + style="text-align:<%= table.columns.customAlignment[column] %>" + <% } %> + > +

<%- column.charAt(0).toUpperCase() + column.slice(1) %>

+
+ style="text-align:<%= table.columns.customAlignment[column] %>" + <% } %> + > + <%- table.data[i][column] %> +
+<% } %> +``` + +It's a good idea to add this to the top of the template to improve the styling of the table: + +```css +/* Table */ +.data-wrapper { + width: 100%; + margin: 0; + padding: 35px 0; +} +.data-table { + width: 100%; + margin: 0; +} +.data-table th { + text-align: left; + padding: 0px 5px; + padding-bottom: 8px; + border-bottom: 1px solid #DEDEDE; +} +.data-table th p { + margin: 0; + font-size: 12px; +} +.data-table td { + text-align: left; + padding: 10px 5px; + font-size: 15px; + line-height: 18px; +} +``` + +## Action Injection + +The following will inject the action link (or button) into the e-mail: + +```html + +<% if (locals.action) { %> + <% action.forEach(function (actionItem) { -%> +

<%- actionItem.instructions %>

+ + <%- actionItem.button.text %> + + <% }) -%> +<% } %> +``` + +It's a good idea to add this to the top of the template to specify a fallback color for the action buttons in case it wasn't provided by the user: + +```html +<% +if (locals.action) { + // Make it possible to override action button color (specify fallback color if no color specified) + locals.action.forEach(function(actionItem) { + if (!actionItem.button.color) { + actionItem.button.color = '#48CFAD'; + } + }); +} +%> +``` + +## Outro Injection + +The following will inject the outro text (string or array) into the e-mail: + +```html +<% if (locals.outro) { %> + <% outro.forEach(function (outroItem) { -%> +

<%- outroItem %>

+ <% }) -%> +<% } %> +``` + +## Signature Injection + +The following will inject the signature phrase (e.g. Yours truly) along with the product name into the e-mail: + +```html +<%- signature %>, +
+<%- product.name %> +``` + +## Copyright Injection + +The following will inject the copyright notice into the e-mail: + +```html +<%- product.copyright %> +``` + +## Go-To Action Injection + +In order to support Gmail's [Go-To Actions](https://developers.google.com/gmail/markup/reference/go-to-action), add the following anywhere within the template: + +```html + +<% if (locals.goToAction) { %> + +<% } %> +``` + +## Text Direction Injection + +In order to support generating RTL e-mails, inject the `textDirection` variable into the `` tag: + +```html + +``` + diff --git a/README.md b/README.md index 8e686b1..9645cab 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Build Status](https://travis-ci.org/matcornic/hermes.svg?branch=master)](https://travis-ci.org/matcornic/hermes) [![Go Report Card](https://goreportcard.com/badge/github.com/matcornic/hermes)](https://goreportcard.com/report/github.com/matcornic/hermes) +[![Go Coverage](http://gocover.io/_badge/github.com/matcornic/hermes/)](http://gocover.io/github.com/matcornic/hermes/) +[![Godoc](https://godoc.org/github.com/matcornic/hermes?status.svg)](https://godoc.org/github.com/matcornic/hermes) Hermes is the Go port of the great [mailgen](https://github.com/eladnava/mailgen) engine for Node.js. Check their work, it's awesome ! It's a package that generates clean, responsive HTML e-mails for sending transactional e-mails (welcome e-mail, reset password e-mails, receipt e-mails and so on). @@ -257,17 +259,7 @@ email := hermes.Email{ ## Contributing -Thanks so much for wanting to help! We really appreciate it. - -* Have an idea for a new feature? -* Want to add a new built-in theme? - -Excellent! You've come to the right place. - -1. If you find a bug or wish to suggest a new feature, please create an issue first -2. Make sure your code & comment conventions are in-line with the project's style -3. Make your commits and PRs as tiny as possible - one feature or bugfix at a time -4. Write detailed commit messages, in-line with the project's commit naming conventions +See [CONTRIBUTING.md](CONTRIBUTING.md) ## License diff --git a/default.go b/default.go index 2c57670..60c9005 100644 --- a/default.go +++ b/default.go @@ -379,7 +379,7 @@ func (dt *Default) HTMLTemplate() string { // PlainTextTemplate returns a Golang template that will generate an plain text email. func (dt *Default) PlainTextTemplate() string { - return `{{.Email.Body.Greeting}} {{.Email.Body.Name}}, + return `{{if .Email.Body.Title }}{{ .Email.Body.Title }}{{ else }}{{ .Email.Body.Greeting }} {{ .Email.Body.Name }},{{ end }}, {{ with .Email.Body.Intros }}{{ range $line := . }}{{ $line }}{{ end }}{{ end }} {{ with .Email.Body.Dictionary }}{{ range $entry := . }} {{ $entry.Key }}: {{ $entry.Value }}{{ end }}{{ end }} @@ -390,7 +390,7 @@ func (dt *Default) PlainTextTemplate() string { {{ $line }}{{ end }}{{ end }} {{.Email.Body.Signature}}, -{{.Hermes.Product.Name}} +{{.Hermes.Product.Name}} - {{.Hermes.Product.Link}} {{.Hermes.Product.Copyright}} ` diff --git a/hermes_test.go b/hermes_test.go index 30284e7..47bc131 100644 --- a/hermes_test.go +++ b/hermes_test.go @@ -5,13 +5,42 @@ import ( "testing" ) -func TestHermes_ok(t *testing.T) { +var testedThemes = []Theme{ + // Insert your new theme here + new(Default), +} +///////////////////////////////////////////////////// +// Every theme should display the same information // +// Find below the tests to check that // +///////////////////////////////////////////////////// + +// Implement this interface when creating a new example checking a common feature of all themes +type Example interface { + // Create the hermes example with data + // Represents the "Given" step in Given/When/Then Workflow + getExample() (h Hermes, email Email) + // Checks the content of the generated HTML email by asserting content presence or not + assertHTMLContent(t *testing.T, s string) + // Checks the content of the generated Plaintext email by asserting content presence or not + assertPlainTextContent(t *testing.T, s string) +} + +// Scenario +type SimpleExample struct { + theme Theme +} + +func (ed *SimpleExample) getExample() (Hermes, Email) { h := Hermes{ + Theme: ed.theme, Product: Product{ - Name: "Hermes", - Link: "http://hermes.com", + Name: "HermesName", + Link: "http://hermes-link.com", + Copyright: "Copyright © Hermes-Test", + Logo: "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png", }, + TextDirection: TDLeftToRight, } email := Email{ @@ -25,6 +54,29 @@ func TestHermes_ok(t *testing.T) { {"Lastname", "Snow"}, {"Birthday", "01/01/283"}, }, + Table: Table{ + Data: [][]Entry{ + { + {Key: "Item", Value: "Golang"}, + {Key: "Description", Value: "Open source programming language that makes it easy to build simple, reliable, and efficient software"}, + {Key: "Price", Value: "$10.99"}, + }, + { + {Key: "Item", Value: "Hermes"}, + {Key: "Description", Value: "Programmatically create beautiful e-mails using Golang."}, + {Key: "Price", Value: "$1.99"}, + }, + }, + Columns: Columns{ + CustomWidth: map[string]string{ + "Item": "20%", + "Price": "15%", + }, + CustomAlignement: map[string]string{ + "Price": "right", + }, + }, + }, Actions: []Action{ { Instructions: "To get started with Hermes, please click here:", @@ -40,21 +92,206 @@ func TestHermes_ok(t *testing.T) { }, }, } + return h, email +} +func (ed *SimpleExample) assertHTMLContent(t *testing.T, r string) { + + // Assert on product + assert.Contains(t, r, "HermesName", "Product: Should find the name of the product in email") + assert.Contains(t, r, "http://hermes-link.com", "Product: Should find the link of the product in email") + assert.Contains(t, r, "Copyright © Hermes-Test", "Product: Should find the Copyright of the product in email") + assert.Contains(t, r, "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png", "Product: Should find the logo of the product in email") + + // Assert on email body + assert.Contains(t, r, "Hi Jon Snow", "Name: Should find the name of the person") + assert.Contains(t, r, "Welcome to Hermes", "Intro: Should have intro") + assert.Contains(t, r, "Birthday", "Dictionary: Should have dictionary") + assert.Contains(t, r, "Open source programming language", "Table: Should have table with first row and first column") + assert.Contains(t, r, "Programmatically create beautiful e-mails using Golang", "Table: Should have table with second row and first column") + assert.Contains(t, r, "$10.99", "Table: Should have table with first row and second column") + assert.Contains(t, r, "$1.99", "Table: Should have table with second row and second column") + assert.Contains(t, r, "started with Hermes", "Action: Should have instruction") + assert.Contains(t, r, "Confirm your account", "Action: Should have button of action") + assert.Contains(t, r, "#22BC66", "Action: Button should have given color") + assert.Contains(t, r, "https://hermes-example.com/confirm?token=d9729feb74992cc3482b350163a1a010", "Action: Button should have link") + assert.Contains(t, r, "Need help, or have questions", "Outro: Should have outro") +} + +func (ed *SimpleExample) assertPlainTextContent(t *testing.T, r string) { + + // Assert on product + assert.Contains(t, r, "HermesName", "Product: Should find the name of the product in email") + assert.Contains(t, r, "http://hermes-link.com", "Product: Should find the link of the product in email") + assert.Contains(t, r, "Copyright © Hermes-Test", "Product: Should find the Copyright of the product in email") + assert.NotContains(t, r, "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png", "Product: Should not find any logo in plain text") + + // Assert on email body + assert.Contains(t, r, "Hi Jon Snow", "Name: Should find the name of the person") + assert.Contains(t, r, "Welcome to Hermes", "Intro: Should have intro") + assert.Contains(t, r, "Birthday", "Dictionary: Should have dictionary") + assert.NotContains(t, r, "Open source programming language", "Table: Not possible to have table in plain text") + assert.NotContains(t, r, "Programmatically create beautiful e-mails using Golang", "Table: Not possible to have table in plain text") + assert.NotContains(t, r, "$10.99", "Table: Not possible to have table in plain text") + assert.NotContains(t, r, "$1.99", "Table: Not possible to have table in plain text") + assert.Contains(t, r, "started with Hermes", "Action: Should have instruction") + assert.NotContains(t, r, "Confirm your account", "Action: Should not have button of action in plain text") + assert.NotContains(t, r, "#22BC66", "Action: Button should not have color in plain text") + assert.Contains(t, r, "https://hermes-example.com/confirm?token=d9729feb74992cc3482b350163a1a010", "Action: Even if button is not possible in plain text, it should have the link") + assert.Contains(t, r, "Need help, or have questions", "Outro: Should have outro") +} + +type WithTitleInsteadOfNameExample struct { + theme Theme +} + +func (ed *WithTitleInsteadOfNameExample) getExample() (Hermes, Email) { + h := Hermes{ + Theme: ed.theme, + Product: Product{ + Name: "Hermes", + Link: "http://hermes.com", + }, + } + + email := Email{ + Body{ + Name: "Jon Snow", + Title: "A new e-mail", + }, + } + return h, email +} + +func (ed *WithTitleInsteadOfNameExample) assertHTMLContent(t *testing.T, r string) { + assert.NotContains(t, r, "Hi Jon Snow", "Name: should not find greetings from Jon Snow because title should be used") + assert.Contains(t, r, "A new e-mail", "Title should be used instead of name") +} + +func (ed *WithTitleInsteadOfNameExample) assertPlainTextContent(t *testing.T, r string) { + assert.NotContains(t, r, "Hi Jon Snow", "Name: should not find greetings from Jon Snow because title should be used") + assert.Contains(t, r, "A new e-mail", "Title shoud be used instead of name") +} + +type WithGreetingDifferentThanDefault struct { + theme Theme +} + +func (ed *WithGreetingDifferentThanDefault) getExample() (Hermes, Email) { + h := Hermes{ + Theme: ed.theme, + Product: Product{ + Name: "Hermes", + Link: "http://hermes.com", + }, + } + + email := Email{ + Body{ + Greeting: "Dear", + Name: "Jon Snow", + }, + } + return h, email +} + +func (ed *WithGreetingDifferentThanDefault) assertHTMLContent(t *testing.T, r string) { + assert.NotContains(t, r, "Hi Jon Snow", "Should not find greetings with 'Hi' which is default") + assert.Contains(t, r, "Dear Jon Snow", "Should have greeting with Dear") +} + +func (ed *WithGreetingDifferentThanDefault) assertPlainTextContent(t *testing.T, r string) { + assert.NotContains(t, r, "Hi Jon Snow", "Should not find greetings with 'Hi' which is default") + assert.Contains(t, r, "Dear Jon Snow", "Should have greeting with Dear") +} + +type WithSignatureDifferentThanDefault struct { + theme Theme +} + +func (ed *WithSignatureDifferentThanDefault) getExample() (Hermes, Email) { + h := Hermes{ + Theme: ed.theme, + Product: Product{ + Name: "Hermes", + Link: "http://hermes.com", + }, + } + + email := Email{ + Body{ + Name: "Jon Snow", + Signature: "Best regards", + }, + } + return h, email +} + +func (ed *WithSignatureDifferentThanDefault) assertHTMLContent(t *testing.T, r string) { + assert.NotContains(t, r, "Yours truly", "Should not find signature with 'Yours truly' which is default") + assert.Contains(t, r, "Best regards", "Should have greeting with Dear") +} + +func (ed *WithSignatureDifferentThanDefault) assertPlainTextContent(t *testing.T, r string) { + assert.NotContains(t, r, "Yours truly", "Should not find signature with 'Yours truly' which is default") + assert.Contains(t, r, "Best regards", "Should have greeting with Dear") +} + +// Test all the themes for the features + +func TestThemeSimple(t *testing.T) { + for _, theme := range testedThemes { + checkExample(t, &SimpleExample{theme}) + } +} + +func TestThemeWithTitleInsteadOfName(t *testing.T) { + for _, theme := range testedThemes { + checkExample(t, &WithTitleInsteadOfNameExample{theme}) + } +} + +func TestThemeWithGreetingDifferentThanDefault(t *testing.T) { + for _, theme := range testedThemes { + checkExample(t, &WithGreetingDifferentThanDefault{theme}) + } +} + +func TestThemeWithGreetingDiffrentThanDefault(t *testing.T) { + for _, theme := range testedThemes { + checkExample(t, &WithSignatureDifferentThanDefault{theme}) + } +} + +func checkExample(t *testing.T, ex Example) { + // Given an example + h, email := ex.getExample() + + // When generating HTML template r, err := h.GenerateHTML(email) t.Log(r) assert.Nil(t, err) assert.NotEmpty(t, r) + // Then asserting HTML is OK + ex.assertHTMLContent(t, r) + + // When generating plain text template r, err = h.GeneratePlainText(email) t.Log(r) assert.Nil(t, err) assert.NotEmpty(t, r) - assert.Equal(t, h.Theme.Name(), "default") + // Then asserting plain text is OK + ex.assertPlainTextContent(t, r) } -func TestHermes_defaultTextDirection(t *testing.T) { +//////////////////////////////////////////// +// Tests on default values for all themes // +// It does not check email content // +//////////////////////////////////////////// + +func TestHermes_TextDirectionAsDefault(t *testing.T) { h := Hermes{ Product: Product{ Name: "Hermes", @@ -88,4 +325,29 @@ func TestHermes_defaultTextDirection(t *testing.T) { _, err := h.GenerateHTML(email) assert.Nil(t, err) assert.Equal(t, h.TextDirection, TDLeftToRight) + assert.Equal(t, h.Theme.Name(), "default") +} + +func TestHermes_Default(t *testing.T) { + h := Hermes{} + setDefaultHermesValues(&h) + email := Email{} + setDefaultEmailValues(&email) + + assert.Equal(t, h.TextDirection, TDLeftToRight) + assert.Equal(t, h.Theme, new(Default)) + assert.Equal(t, h.Product.Name, "Hermes") + assert.Equal(t, h.Product.Copyright, "Copyright © 2017 Hermes. All rights reserved.") + + assert.Empty(t, email.Body.Actions) + assert.Empty(t, email.Body.Dictionary) + assert.Empty(t, email.Body.Intros) + assert.Empty(t, email.Body.Outros) + assert.Empty(t, email.Body.Table.Data) + assert.Empty(t, email.Body.Table.Columns.CustomWidth) + assert.Empty(t, email.Body.Table.Columns.CustomAlignement) + + assert.Equal(t, email.Body.Greeting, "Hi") + assert.Equal(t, email.Body.Signature, "Yours truly") + assert.Empty(t, email.Body.Title) }