html_email_creator 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/README.markdown +5 -0
- data/Rakefile +7 -0
- data/bin/html_email_creator +35 -0
- data/html_email_creator.gemspec +41 -0
- data/lib/html_email_creator/email.rb +93 -0
- data/lib/html_email_creator/email_creator.rb +45 -0
- data/lib/html_email_creator/email_version.rb +26 -0
- data/lib/html_email_creator/extensions.rb +46 -0
- data/lib/html_email_creator/filters.rb +9 -0
- data/lib/html_email_creator/formatter.rb +30 -0
- data/lib/html_email_creator/formatters/formatter.rb +31 -0
- data/lib/html_email_creator/formatters/html_email.rb +24 -0
- data/lib/html_email_creator/formatters/markdown.rb +24 -0
- data/lib/html_email_creator/formatters/plain_text_email.rb +22 -0
- data/lib/html_email_creator/formatters/unknown_formatter.rb +17 -0
- data/lib/html_email_creator/helper.rb +25 -0
- data/lib/html_email_creator/information.rb +4 -0
- data/lib/html_email_creator/layout.rb +15 -0
- data/lib/html_email_creator/processor.rb +125 -0
- data/lib/html_email_creator/settings.rb +95 -0
- data/lib/html_email_creator/tags/include_tag.rb +71 -0
- data/lib/html_email_creator/version.rb +3 -0
- data/lib/html_email_creator.rb +46 -0
- data/spec/fixtures/complex_with_config/.html_config.yaml +11 -0
- data/spec/fixtures/complex_with_config/Emails/polite_email.yaml +7 -0
- data/spec/fixtures/complex_with_config/Includes/Emails/love.md +1 -0
- data/spec/fixtures/complex_with_config/Includes/Footers/polite.md +3 -0
- data/spec/fixtures/complex_with_config/Layouts/.keep +0 -0
- data/spec/fixtures/complex_with_config/Layouts/basic.liquid +25 -0
- data/spec/fixtures/complex_with_config/Output/.keep +0 -0
- data/spec/fixtures/default_config/Emails/Newsletter/.keep +0 -0
- data/spec/fixtures/default_config/Layouts/.keep +0 -0
- data/spec/fixtures/default_config/Output/.keep +0 -0
- data/spec/fixtures/with_config/.html_config.yaml +11 -0
- data/spec/fixtures/with_config/Emails/first_email.yaml +7 -0
- data/spec/fixtures/with_config/Includes/Emails/barfoo.md +1 -0
- data/spec/fixtures/with_config/Includes/Emails/foobar.md +1 -0
- data/spec/fixtures/with_config/Includes/Quotes/henry_ford.txt +1 -0
- data/spec/fixtures/with_config/Layouts/.keep +0 -0
- data/spec/fixtures/with_config/Layouts/simple.liquid +7 -0
- data/spec/fixtures/with_config/Output/.keep +0 -0
- data/spec/html_email_creator/email_creator_spec.rb +119 -0
- data/spec/html_email_creator/email_spec.rb +29 -0
- data/spec/html_email_creator/formatter_spec.rb +40 -0
- data/spec/html_email_creator/layout_spec.rb +55 -0
- data/spec/html_email_creator/processor_spec.rb +111 -0
- data/spec/html_email_creator/settings_spec.rb +70 -0
- data/spec/html_email_creator/tags/include_tag_spec.rb +11 -0
- data/spec/spec_helper.rb +32 -0
- metadata +244 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'kramdown'
|
2
|
+
|
3
|
+
module HtmlEmailCreator
|
4
|
+
module Formatters
|
5
|
+
class HtmlEmail < Formatter
|
6
|
+
def self.extension
|
7
|
+
"html"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.id
|
11
|
+
:html_email
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(html, settings)
|
15
|
+
super
|
16
|
+
@processor = HtmlEmailCreator::Processor.new(html, settings)
|
17
|
+
end
|
18
|
+
|
19
|
+
def format
|
20
|
+
@output ||= @processor.to_html
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'kramdown'
|
2
|
+
|
3
|
+
module HtmlEmailCreator
|
4
|
+
module Formatters
|
5
|
+
class Markdown < Formatter
|
6
|
+
def self.extension
|
7
|
+
"md"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.id
|
11
|
+
:md
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(text, settings)
|
15
|
+
super
|
16
|
+
@document = Kramdown::Document.new(text)
|
17
|
+
end
|
18
|
+
|
19
|
+
def format
|
20
|
+
@output ||= @document.to_html.strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module HtmlEmailCreator
|
2
|
+
module Formatters
|
3
|
+
class PlainTextEmail < Formatter
|
4
|
+
def self.extension
|
5
|
+
"txt"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.id
|
9
|
+
:plain_text_email
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(html, settings)
|
13
|
+
super
|
14
|
+
@processor = HtmlEmailCreator::Processor.new(html, settings)
|
15
|
+
end
|
16
|
+
|
17
|
+
def format
|
18
|
+
@output ||= @processor.to_plain_text
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HtmlEmailCreator
|
2
|
+
module Formatters
|
3
|
+
class UnknownFormatter < Formatter
|
4
|
+
def self.extension
|
5
|
+
"txt"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.id
|
9
|
+
:unknown
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(text, settings = HtmlEmailCreator.settings)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HtmlEmailCreator
|
2
|
+
|
3
|
+
module Helper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# Find recursively starting from start_from_dir and continues towards a root.
|
8
|
+
def find_recursively(start_from_dir, dir_or_file, default_if_not_found = nil)
|
9
|
+
current_file = File.join(start_from_dir, dir_or_file)
|
10
|
+
if File.exists?(current_file)
|
11
|
+
current_file
|
12
|
+
else
|
13
|
+
next_file = File.dirname(start_from_dir)
|
14
|
+
if start_from_dir == next_file
|
15
|
+
return default_if_not_found
|
16
|
+
end
|
17
|
+
|
18
|
+
# continue searching
|
19
|
+
find_recursively(next_file, dir_or_file, default_if_not_found)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "liquid"
|
2
|
+
|
3
|
+
module HtmlEmailCreator
|
4
|
+
class Layout
|
5
|
+
def initialize(text, default_data = HtmlEmailCreator.settings.extension_data)
|
6
|
+
@text = text
|
7
|
+
@template = Liquid::Template.parse(text)
|
8
|
+
@default_data = default_data
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_html(data = {}, *filters)
|
12
|
+
@template.render(@default_data.merge(data), :filters => [HtmlEmailCreator::Filters] + filters)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'inline-style'
|
3
|
+
|
4
|
+
module HtmlEmailCreator
|
5
|
+
class Processor
|
6
|
+
def initialize(email_string, settings = HtmlEmailCreator.settings)
|
7
|
+
@email = email_string
|
8
|
+
@settings = settings
|
9
|
+
end
|
10
|
+
|
11
|
+
def original_email
|
12
|
+
@email
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_html
|
16
|
+
return @processed_html_email if @processed_html_email
|
17
|
+
|
18
|
+
doc = Nokogiri::HTML(@email)
|
19
|
+
headers = ["h1", "h2", "h3", "h4", "h5"]
|
20
|
+
|
21
|
+
# wrap headers with div
|
22
|
+
|
23
|
+
headers.each do |hx|
|
24
|
+
nodes = doc.css hx
|
25
|
+
nodes.wrap("<div class='#{hx}'></div>")
|
26
|
+
end
|
27
|
+
|
28
|
+
# replace headers with divs
|
29
|
+
|
30
|
+
headers.each do |hx|
|
31
|
+
doc.css(hx).each { |h| h.name = "div" }
|
32
|
+
end
|
33
|
+
|
34
|
+
# replace paragraphs with divs
|
35
|
+
|
36
|
+
#doc.css("p").each { |p| p.name = "div" }
|
37
|
+
|
38
|
+
# store file
|
39
|
+
|
40
|
+
html_email = doc.to_html
|
41
|
+
|
42
|
+
inlined_html_email = InlineStyle.process(html_email)
|
43
|
+
|
44
|
+
# Then remove escaping of Aweber templates
|
45
|
+
@processed_html_email = if @settings.built_in_extensions.include?("aweber")
|
46
|
+
inlined_html_email.gsub(/%7B/, '{').gsub(/%7D/, '}').gsub(/!global%20/, '!global ')
|
47
|
+
else
|
48
|
+
inlined_html_email
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_plain_text
|
53
|
+
return @processed_plain_text_email if @processed_plain_text_email
|
54
|
+
|
55
|
+
doc = Nokogiri::HTML(to_html)
|
56
|
+
doc.css('style').each { |node| node.remove }
|
57
|
+
doc.css('title').each { |node| node.remove }
|
58
|
+
doc.css('script').each { |node| node.remove }
|
59
|
+
doc.css('link').each { |node| node.remove }
|
60
|
+
doc.css('a').each do |node|
|
61
|
+
img = node.at_css('img')
|
62
|
+
node.content = if img
|
63
|
+
"#{img['alt']} (#{node['href']})"
|
64
|
+
else
|
65
|
+
"#{node.content} (#{node['href']})"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
doc.css('img').each do |node|
|
69
|
+
node.content = "#{node['alt']} (#{node['src']})"
|
70
|
+
end
|
71
|
+
doc.css('strong').each do |node|
|
72
|
+
node.content = "*#{node.content}*"
|
73
|
+
end
|
74
|
+
doc.css('li').each { |node| node.content = "- #{node.content.strip}" }
|
75
|
+
|
76
|
+
# format all content that must have an empty line on top of them
|
77
|
+
|
78
|
+
doc.css('div.h1 div').each do |node|
|
79
|
+
node.content = "\n#{'=' * 77}\n#{node.content}"
|
80
|
+
end
|
81
|
+
doc.css('div.h2 div').each do |node|
|
82
|
+
node.content = "\n#{'-' * 77}\n#{node.content}"
|
83
|
+
end
|
84
|
+
doc.css('div.h3 div').each do |node|
|
85
|
+
node.content = "\n#{'- ' * 39}\n#{node.content}"
|
86
|
+
end
|
87
|
+
doc.css('div.h4 div').each do |node|
|
88
|
+
node.content = "\n#{node.content}"
|
89
|
+
end
|
90
|
+
doc.css('p').each do |node|
|
91
|
+
node.content = "\n#{node.content}"
|
92
|
+
end
|
93
|
+
|
94
|
+
doc.css('table.inline').each do |table|
|
95
|
+
content = []
|
96
|
+
table.css('tr').each do |row|
|
97
|
+
# each_index method is missing
|
98
|
+
i = 0
|
99
|
+
row.css('td').each do |column|
|
100
|
+
empty_column = column.content.strip.gsub(/^\p{Space}+|\p{Space}+$/, "").empty?
|
101
|
+
if content[i]
|
102
|
+
content[i] = content[i] + "\n* #{column.content}" unless empty_column
|
103
|
+
else
|
104
|
+
content[i] = "\n#{column.content}" unless empty_column
|
105
|
+
end
|
106
|
+
i = i + 1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
table.content = "#{content.join("\n")}\n"
|
111
|
+
end
|
112
|
+
|
113
|
+
# remove unnecessary whitespaces and empty lines
|
114
|
+
previous = ""
|
115
|
+
@processed_plain_text_email = doc.css('body').text.split("\n").map do |line|
|
116
|
+
# Nokogiri uses UTF-8 whitespace character for   and strip doesn't support those
|
117
|
+
res = line.gsub(/^\p{Space}+|\p{Space}+$/, "")
|
118
|
+
new_previous = res
|
119
|
+
res = nil if res.empty? && previous.empty?
|
120
|
+
previous = new_previous
|
121
|
+
res
|
122
|
+
end.compact.join("\n")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module HtmlEmailCreator
|
2
|
+
class Settings
|
3
|
+
|
4
|
+
# Create settings configuration file.
|
5
|
+
#
|
6
|
+
# If the root is not set, the configuration is not searched from the file system
|
7
|
+
# but instead the defaults are used.
|
8
|
+
def initialize(root = nil)
|
9
|
+
@root = root
|
10
|
+
@root ||= File.expand_path('~')
|
11
|
+
@config = create_configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def layouts_path
|
15
|
+
@config["layouts_path"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def output_path
|
19
|
+
@config["output_path"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def emails_path
|
23
|
+
@config["emails_path"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def includes_path
|
27
|
+
@config["includes_path"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def cdn_url
|
31
|
+
@config["cdn_url"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def extension_data
|
35
|
+
return @extension_data if @extension_data
|
36
|
+
extensions = HtmlEmailCreator::Extensions.new(self)
|
37
|
+
built_in_data = extensions.built_in(built_in_extensions)
|
38
|
+
# use built in data for creating custom data
|
39
|
+
custom_data = extensions.custom(built_in_data, custom_extensions)
|
40
|
+
@extension_data = built_in_data.merge(custom_data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def built_in_extensions
|
44
|
+
(@config["extensions"] || {})["built_in"] || []
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def custom_extensions
|
50
|
+
(@config["extensions"] || {})["custom"] || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_configuration
|
54
|
+
config_file = find_config_file
|
55
|
+
if config_file
|
56
|
+
config_root_dir = File.dirname(config_file)
|
57
|
+
loaded_config = YAML.load_file(config_file)
|
58
|
+
# fill missing values with defaults if missing
|
59
|
+
default_config.each_pair do |key, value|
|
60
|
+
loaded_config[key] = value unless loaded_config[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
# make absolute paths if is relative for all _path ending keys
|
64
|
+
loaded_config.each_pair do |key, value|
|
65
|
+
if key.match(/_path$/) && !value.match(/^\//)
|
66
|
+
loaded_config[key] = File.join(config_root_dir, value)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
loaded_config
|
71
|
+
else
|
72
|
+
default_config
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def default_config
|
77
|
+
{
|
78
|
+
"layouts_path" => find_dir("Layouts"),
|
79
|
+
"output_path" => find_dir("Output"),
|
80
|
+
"emails_path" => find_dir("Emails"),
|
81
|
+
"includes_path" => find_dir("Includes"),
|
82
|
+
"cdn_url" => "",
|
83
|
+
"extensions" => {}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_dir(dir)
|
88
|
+
HtmlEmailCreator::Helper.find_recursively(@root, dir, File.join(@root, dir))
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_config_file
|
92
|
+
HtmlEmailCreator::Helper.find_recursively(@root, ".html_config.yaml")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'liquid'
|
2
|
+
|
3
|
+
module HtmlEmailCreator
|
4
|
+
|
5
|
+
class IncludeTag < Liquid::Tag
|
6
|
+
Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/
|
7
|
+
|
8
|
+
def initialize(tag_name, markup, tokens)
|
9
|
+
if markup =~ Syntax
|
10
|
+
|
11
|
+
@template_name = $1
|
12
|
+
@variable_name = $3
|
13
|
+
@attributes = {}
|
14
|
+
|
15
|
+
markup.scan(Liquid::TagAttributes) do |key, value|
|
16
|
+
@attributes[key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
else
|
20
|
+
raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
|
21
|
+
end
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse(tokens)
|
27
|
+
end
|
28
|
+
|
29
|
+
def render(context)
|
30
|
+
settings = find_settings(context)
|
31
|
+
source = read_template_from_file_system(context, settings)
|
32
|
+
partial = Liquid::Template.parse(source)
|
33
|
+
variable = context[@variable_name || @template_name[1..-2]]
|
34
|
+
|
35
|
+
context.stack do
|
36
|
+
@attributes.each do |key, value|
|
37
|
+
context[key] = context[value]
|
38
|
+
end
|
39
|
+
|
40
|
+
if variable.is_a?(Array)
|
41
|
+
variable.collect do |variable|
|
42
|
+
context[@template_name[1..-2]] = variable
|
43
|
+
partial.render(context)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
context[@template_name[1..-2]] = variable
|
47
|
+
partial.render(context)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def find_settings(context)
|
55
|
+
(context.registers[:settings] || HtmlEmailCreator.settings)
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_template_from_file_system(context, settings)
|
59
|
+
template = File.join(settings.includes_path, context[@template_name])
|
60
|
+
if File.exists?(template)
|
61
|
+
# run through a formatter
|
62
|
+
formatter = HtmlEmailCreator::Formatter.new(IO.read(template), settings)
|
63
|
+
formatter.find_by_filename(template).format
|
64
|
+
else
|
65
|
+
"Included file '#{template}' not found."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Liquid::Template.register_tag('include', IncludeTag)
|
71
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
def require_all(path)
|
2
|
+
glob = File.join(File.dirname(__FILE__), path, '*.rb')
|
3
|
+
Dir[glob].each do |f|
|
4
|
+
require f
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_all 'html_email_creator/tags'
|
9
|
+
require_all 'html_email_creator/formatters'
|
10
|
+
|
11
|
+
module HtmlEmailCreator
|
12
|
+
|
13
|
+
autoload :Email, "html_email_creator/email"
|
14
|
+
autoload :EmailCreator, "html_email_creator/email_creator"
|
15
|
+
autoload :EmailVersion, "html_email_creator/email_version"
|
16
|
+
autoload :Extensions, "html_email_creator/extensions"
|
17
|
+
autoload :Filters, 'html_email_creator/filters'
|
18
|
+
autoload :Formatter, 'html_email_creator/formatter'
|
19
|
+
autoload :Helper, 'html_email_creator/helper'
|
20
|
+
autoload :Layout, 'html_email_creator/layout'
|
21
|
+
autoload :Markdown, 'html_email_creator/markdown'
|
22
|
+
autoload :Processor, 'html_email_creator/processor'
|
23
|
+
autoload :Renderer, 'html_email_creator/renderer'
|
24
|
+
autoload :Settings, 'html_email_creator/settings'
|
25
|
+
autoload :Version, 'html_email_creator/version'
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_writer :settings
|
29
|
+
|
30
|
+
def current_dir
|
31
|
+
Dir.pwd
|
32
|
+
end
|
33
|
+
|
34
|
+
def settings
|
35
|
+
@settings ||= read_settings(current_dir)
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_settings(from_email_dir = current_dir)
|
39
|
+
@settings = read_settings(from_email_dir)
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_settings(dir)
|
43
|
+
Settings.new(dir)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|