html_email_creator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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