html_email_creator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|