Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,53 @@ end
```


## Markdown support

Courrier supports rendering markdown content to HTML when a markdown gem is available. Simply bundle any supported markdown gem (`redcarpet`, `kramdown` or `commonmarker`) and it will be used.


### Markdown methods

Define a `markdown` method in your email class:
```ruby
class OrderEmail < Courrier::Email
def subject = "Your order is ready!"

def markdown
<<~MARKDOWN
# Hello #{name}!

Your order **##{order_id}** is ready for pickup.

## Order Details
- Item: #{item_name}
- Price: #{price}
MARKDOWN
end
end
```


### Markdown templates

Create markdown template files alongside your email class:
- `app/emails/order_email.md.erb`
- `app/emails/order_email.markdown.erb`

```erb
<!-- app/emails/order_email.md.erb -->
# Hello <%= name %>!

Your order **#<%= order_id %>** is ready for pickup.

## Order Details
- Item: <%= item_name %>
- Price: <%= price %>
```

Method definitions take precedence over template files. You can mix approaches. For example, define `text` in a method and use a markdown template for HTML content.


### Auto-generate text from HTML

Automatically generate plain text versions from your HTML emails:
Expand Down
22 changes: 21 additions & 1 deletion lib/courrier/email.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "courrier/email/address"
require "courrier/jobs/email_delivery_job" if defined?(Rails)
require "courrier/email/layouts"
require "courrier/markdown"
require "courrier/email/options"
require "courrier/email/provider"

Expand Down Expand Up @@ -137,7 +138,9 @@ def delivery_disabled?

def method_missing(name, *)
if name == :text || name == :html
render_template(name.to_s)
render_template(name.to_s).tap do |result|
return result || markdown_rendered if name == :html
end
else
@context_options[name]
end
Expand All @@ -149,6 +152,23 @@ def render_template(format)
File.exist?(template_path) ? ERB.new(File.read(template_path)).result(binding) : nil
end

def render_markdown_template
%w[md markdown].each do |ext|
template_path = template_file_path(ext)

return ERB.new(File.read(template_path)).result(binding) if File.exist?(template_path)
end

nil
end

def markdown_rendered
return unless Courrier::Markdown.available?

markdown_content = render_markdown_template || (respond_to?(:markdown, true) ? markdown : nil)
Courrier::Markdown.render(markdown_content) if markdown_content
end

def template_file_path(format)
class_path = self.class.name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase

Expand Down
52 changes: 52 additions & 0 deletions lib/courrier/markdown.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Courrier
class Markdown
class << self
def available?
defined?(::Redcarpet) || defined?(::Kramdown) || defined?(::Commonmarker)
end

def render(text)
return unless available?

parser.parse(text.to_s)
end

private

def parser
@parser ||= available_parser.new
end

def available_parser
return RedcarpetParser if defined?(::Redcarpet)
return KramdownParser if defined?(::Kramdown)
return CommonmarkerParser if defined?(::Commonmarker)

Parser
end
end

class Parser
def parse(text) = text.to_s
end

class RedcarpetParser < Parser
def parse(text)
renderer = Redcarpet::Render::HTML.new
markdown = Redcarpet::Markdown.new(renderer)

markdown.render(text)
end
end

class KramdownParser < Parser
def parse(text) = Kramdown::Document.new(text).to_html
end

class CommonmarkerParser < Parser
def parse(text) = Commonmarker.to_html(text)
end
end
end
112 changes: 80 additions & 32 deletions test/courrier/email_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ def test_abstract_methods_raise_error
email = Courrier::Email.new(from: "devs@railsdesigner.com", to: "recipient@railsdesigner.com")

assert_nil email.subject

assert_nil email.text

assert_nil email.html
end

Expand Down Expand Up @@ -100,48 +98,98 @@ def test_url_generation_with_missing_host
end

def test_template_rendering_with_files
email_path = "tmp/test_emails"
email_path = "tmp/test_emails"

FileUtils.mkdir_p(email_path)
File.write("#{email_path}/test_email_with_templates.text.erb", "Hello <%= name %>!")
File.write("#{email_path}/test_email_with_templates.html.erb", "<p>Hello <strong><%= name %></strong>!</p>")
FileUtils.mkdir_p(email_path)
File.write("#{email_path}/test_email_with_templates.text.erb", "Hello <%= name %>!")
File.write("#{email_path}/test_email_with_templates.html.erb", "<p>Hello <strong><%= name %></strong>!</p>")

Courrier.configure do |config|
config.email_path = email_path
end

Courrier.configure do |config|
config.email_path = email_path
email = TestEmailWithTemplates.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com",
name: "World"
)

assert_equal "Hello World!", email.text
assert_equal "<p>Hello <strong>World</strong>!</p>", email.html

FileUtils.rm_rf(email_path)
end

email = TestEmailWithTemplates.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com",
name: "World"
)
def test_method_takes_precedence_over_template
email_path = "tmp/test_emails"

assert_equal "Hello World!", email.text
assert_equal "<p>Hello <strong>World</strong>!</p>", email.html
FileUtils.mkdir_p(email_path)
File.write("#{email_path}/test_email_with_mixed_content.text.erb", "Template text")
File.write("#{email_path}/test_email_with_mixed_content.html.erb", "<p>Template HTML</p>")

FileUtils.rm_rf(email_path)
end
Courrier.configure do |config|
config.email_path = email_path
end

email = TestEmailWithMixedContent.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com"
)

def test_method_takes_precedence_over_template
email_path = "tmp/test_emails"
assert_equal "Method text", email.text
assert_equal "<p>Template HTML</p>", email.html

FileUtils.rm_rf(email_path)
end

FileUtils.mkdir_p(email_path)
File.write("#{email_path}/test_email_with_mixed_content.text.erb", "Template text")
File.write("#{email_path}/test_email_with_mixed_content.html.erb", "<p>Template HTML</p>")
def test_markdown_method_renders_when_gem_available
with_mocked_markdown do
email = EmailWithMarkdown.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com",
name: "World"
)

Courrier.configure do |config|
config.email_path = email_path
expected = "<p># Hello World!\n\nOrder **123** is ready.\n</p>"
assert_equal expected, email.html
end
end

email = TestEmailWithMixedContent.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com"
)
def test_markdown_template_renders_when_gem_available
with_mocked_markdown do
email_path = "tmp/test_emails"

assert_equal "Method text", email.text
assert_equal "<p>Template HTML</p>", email.html
FileUtils.mkdir_p(email_path)
File.write("#{email_path}/email_with_markdown_template.md.erb", "Hello <%= name %>!")

FileUtils.rm_rf(email_path)
end
Courrier.configure do |config|
config.email_path = email_path
end

email = EmailWithMarkdownTemplate.new(
to: "recipient@railsdesigner.com",
from: "devs@railsdesigner.com",
name: "World"
)

assert_equal "<p>Hello World!</p>", email.html

FileUtils.rm_rf(email_path)
end
end

private

def with_mocked_markdown
original_available = Courrier::Markdown.method(:available?)
original_render = Courrier::Markdown.method(:render)

Courrier::Markdown.define_singleton_method(:available?) { true }
Courrier::Markdown.define_singleton_method(:render) { |text| "<p>#{text}</p>" }

yield
ensure
Courrier::Markdown.define_singleton_method(:available?, original_available)
Courrier::Markdown.define_singleton_method(:render, original_render)
end
end
27 changes: 27 additions & 0 deletions test/fixtures/email_with_markdown.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class EmailWithMarkdown < Courrier::Email
def subject = "Markdown Test"

def markdown
<<~MARKDOWN
# Hello #{name}!

Order **#{order_id || "123"}** is ready.
MARKDOWN
end
end

class EmailWithMarkdownTemplate < Courrier::Email
def subject = "Markdown Template Test"
end

class EmailWithHtmlAndMarkdown < Courrier::Email
def subject = "HTML vs Markdown Test"

def html = "<p>HTML method</p>"

def markdown
<<~MARKDOWN
# This should not be used
MARKDOWN
end
end