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.
Files changed (53) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +49 -0
  4. data/README.markdown +5 -0
  5. data/Rakefile +7 -0
  6. data/bin/html_email_creator +35 -0
  7. data/html_email_creator.gemspec +41 -0
  8. data/lib/html_email_creator/email.rb +93 -0
  9. data/lib/html_email_creator/email_creator.rb +45 -0
  10. data/lib/html_email_creator/email_version.rb +26 -0
  11. data/lib/html_email_creator/extensions.rb +46 -0
  12. data/lib/html_email_creator/filters.rb +9 -0
  13. data/lib/html_email_creator/formatter.rb +30 -0
  14. data/lib/html_email_creator/formatters/formatter.rb +31 -0
  15. data/lib/html_email_creator/formatters/html_email.rb +24 -0
  16. data/lib/html_email_creator/formatters/markdown.rb +24 -0
  17. data/lib/html_email_creator/formatters/plain_text_email.rb +22 -0
  18. data/lib/html_email_creator/formatters/unknown_formatter.rb +17 -0
  19. data/lib/html_email_creator/helper.rb +25 -0
  20. data/lib/html_email_creator/information.rb +4 -0
  21. data/lib/html_email_creator/layout.rb +15 -0
  22. data/lib/html_email_creator/processor.rb +125 -0
  23. data/lib/html_email_creator/settings.rb +95 -0
  24. data/lib/html_email_creator/tags/include_tag.rb +71 -0
  25. data/lib/html_email_creator/version.rb +3 -0
  26. data/lib/html_email_creator.rb +46 -0
  27. data/spec/fixtures/complex_with_config/.html_config.yaml +11 -0
  28. data/spec/fixtures/complex_with_config/Emails/polite_email.yaml +7 -0
  29. data/spec/fixtures/complex_with_config/Includes/Emails/love.md +1 -0
  30. data/spec/fixtures/complex_with_config/Includes/Footers/polite.md +3 -0
  31. data/spec/fixtures/complex_with_config/Layouts/.keep +0 -0
  32. data/spec/fixtures/complex_with_config/Layouts/basic.liquid +25 -0
  33. data/spec/fixtures/complex_with_config/Output/.keep +0 -0
  34. data/spec/fixtures/default_config/Emails/Newsletter/.keep +0 -0
  35. data/spec/fixtures/default_config/Layouts/.keep +0 -0
  36. data/spec/fixtures/default_config/Output/.keep +0 -0
  37. data/spec/fixtures/with_config/.html_config.yaml +11 -0
  38. data/spec/fixtures/with_config/Emails/first_email.yaml +7 -0
  39. data/spec/fixtures/with_config/Includes/Emails/barfoo.md +1 -0
  40. data/spec/fixtures/with_config/Includes/Emails/foobar.md +1 -0
  41. data/spec/fixtures/with_config/Includes/Quotes/henry_ford.txt +1 -0
  42. data/spec/fixtures/with_config/Layouts/.keep +0 -0
  43. data/spec/fixtures/with_config/Layouts/simple.liquid +7 -0
  44. data/spec/fixtures/with_config/Output/.keep +0 -0
  45. data/spec/html_email_creator/email_creator_spec.rb +119 -0
  46. data/spec/html_email_creator/email_spec.rb +29 -0
  47. data/spec/html_email_creator/formatter_spec.rb +40 -0
  48. data/spec/html_email_creator/layout_spec.rb +55 -0
  49. data/spec/html_email_creator/processor_spec.rb +111 -0
  50. data/spec/html_email_creator/settings_spec.rb +70 -0
  51. data/spec/html_email_creator/tags/include_tag_spec.rb +11 -0
  52. data/spec/spec_helper.rb +32 -0
  53. 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,4 @@
1
+ module HtmlEmailCreator
2
+ SUMMARY = "An easy way to create HTML and plain text emails!"
3
+ DESCRIPTION = "An easy way to create HTML and plain text emails using Markdown markup and Liquid layouts." unless defined?(::HtmlEmailCreator::DESCRIPTION)
4
+ 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 &nbsp 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,3 @@
1
+ module HtmlEmailCreator
2
+ VERSION = "1.0.0" unless defined?(::HtmlEmailCreator::VERSION)
3
+ 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