antwort 0.0.12

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +19 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +249 -0
  7. data/Gemfile +3 -0
  8. data/Guardfile +14 -0
  9. data/README.md +108 -0
  10. data/Rakefile +14 -0
  11. data/antwort.gemspec +45 -0
  12. data/bin/antwort +5 -0
  13. data/lib/antwort.rb +13 -0
  14. data/lib/antwort/builder.rb +8 -0
  15. data/lib/antwort/builder/builder.rb +104 -0
  16. data/lib/antwort/builder/email.rb +61 -0
  17. data/lib/antwort/builder/flattener.rb +37 -0
  18. data/lib/antwort/builder/helpers/logic.rb +82 -0
  19. data/lib/antwort/builder/helpers/sanitizers.rb +29 -0
  20. data/lib/antwort/builder/partial.rb +80 -0
  21. data/lib/antwort/builder/style.rb +59 -0
  22. data/lib/antwort/cli.rb +7 -0
  23. data/lib/antwort/cli/cli.rb +275 -0
  24. data/lib/antwort/cli/helpers.rb +44 -0
  25. data/lib/antwort/cli/send.rb +79 -0
  26. data/lib/antwort/cli/upload.rb +89 -0
  27. data/lib/antwort/helpers.rb +19 -0
  28. data/lib/antwort/server.rb +70 -0
  29. data/lib/antwort/server/assets.rb +23 -0
  30. data/lib/antwort/server/helpers.rb +67 -0
  31. data/lib/antwort/server/markup.rb +39 -0
  32. data/lib/antwort/version.rb +3 -0
  33. data/spec/builder/builder_spec.rb +30 -0
  34. data/spec/builder/email_spec.rb +21 -0
  35. data/spec/builder/flattener_spec.rb +64 -0
  36. data/spec/builder/helpers_logic_spec.rb +244 -0
  37. data/spec/builder/partial_spec.rb +87 -0
  38. data/spec/builder/style_spec.rb +66 -0
  39. data/spec/cli/helpers_spec.rb +60 -0
  40. data/spec/cli/upload_spec.rb +47 -0
  41. data/spec/cli_spec.rb +46 -0
  42. data/spec/fixtures/assets/images/1-demo/placeholder.png +0 -0
  43. data/spec/fixtures/assets/images/newsletter/placeholder.png +0 -0
  44. data/spec/fixtures/assets/images/shared/placeholder-grey.png +0 -0
  45. data/spec/fixtures/assets/images/shared/placeholder-white.png +0 -0
  46. data/spec/fixtures/build/demo-123456/build.html +7 -0
  47. data/spec/fixtures/build/demo-123457/build.html +7 -0
  48. data/spec/fixtures/build/demo-bar-123/build.html +7 -0
  49. data/spec/fixtures/build/foo-1/build.html +7 -0
  50. data/spec/fixtures/data/1-demo.yml +3 -0
  51. data/spec/fixtures/emails/1-demo/index.html.erb +9 -0
  52. data/spec/fixtures/emails/2-no-layout/index.html.erb +6 -0
  53. data/spec/fixtures/emails/3-no-title/index.html.erb +1 -0
  54. data/spec/fixtures/views/404.html.erb +1 -0
  55. data/spec/fixtures/views/index.html.erb +14 -0
  56. data/spec/fixtures/views/layout.erb +8 -0
  57. data/spec/fixtures/views/server.erb +5 -0
  58. data/spec/server_spec.rb +54 -0
  59. data/spec/spec_helper.rb +38 -0
  60. data/spec/support/capture.rb +17 -0
  61. data/template/email/css/include.scss +5 -0
  62. data/template/email/css/inline.scss +5 -0
  63. data/template/email/email.html.erb +11 -0
  64. data/template/email/images/.empty_directory +0 -0
  65. data/template/project/.env.sample +21 -0
  66. data/template/project/.gitignore.tt +17 -0
  67. data/template/project/.ruby-version +1 -0
  68. data/template/project/Gemfile.tt +9 -0
  69. data/template/project/Guardfile +9 -0
  70. data/template/project/assets/css/demo/include.scss +3 -0
  71. data/template/project/assets/css/demo/inline.scss +33 -0
  72. data/template/project/assets/css/server.scss +167 -0
  73. data/template/project/assets/css/shared/_base.scss +64 -0
  74. data/template/project/assets/css/shared/_mixins.scss +25 -0
  75. data/template/project/assets/css/shared/_reset.scss +59 -0
  76. data/template/project/assets/css/shared/_vars.scss +12 -0
  77. data/template/project/assets/css/shared/include.scss +23 -0
  78. data/template/project/assets/css/shared/inline.scss +9 -0
  79. data/template/project/assets/images/.gitkeep +0 -0
  80. data/template/project/assets/images/shared/placeholder.png +0 -0
  81. data/template/project/build/.empty_directory +0 -0
  82. data/template/project/data/.empty_directory +0 -0
  83. data/template/project/data/config.yml +3 -0
  84. data/template/project/data/demo.yml +4 -0
  85. data/template/project/emails/demo/_partial.html.erb +9 -0
  86. data/template/project/emails/demo/index.html.erb +54 -0
  87. data/template/project/views/404.html.erb +8 -0
  88. data/template/project/views/index.html.erb +18 -0
  89. data/template/project/views/layout.erb +38 -0
  90. data/template/project/views/markup/_button.html.erb +5 -0
  91. data/template/project/views/markup/_image_tag.html.erb +10 -0
  92. data/template/project/views/server.erb +32 -0
  93. metadata +443 -0
data/antwort.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'antwort/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'antwort'
8
+ s.version = Antwort::VERSION
9
+ s.summary = 'Antwort Generator'
10
+ s.description = 'E-Mail development, build and test system.'
11
+ s.authors = ['Julie Ng']
12
+ s.email = 'hello@antwort.co'
13
+ s.homepage = 'https://antwort.co'
14
+ s.license = 'MIT'
15
+
16
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
+ s.require_paths = ['lib']
20
+ s.required_ruby_version = '~> 2.0'
21
+
22
+ s.add_runtime_dependency 'rake'
23
+ s.add_runtime_dependency 'thor'
24
+ s.add_runtime_dependency 'sinatra'
25
+ s.add_runtime_dependency 'sinatra-contrib'
26
+ s.add_runtime_dependency 'sinatra-partial'
27
+ s.add_runtime_dependency 'dotenv'
28
+ s.add_runtime_dependency 'htmlentities'
29
+ s.add_runtime_dependency 'mail'
30
+ s.add_runtime_dependency 'nokogiri'
31
+ s.add_runtime_dependency 'roadie', '>= 3.0.0'
32
+
33
+ s.add_runtime_dependency 'sass'
34
+ s.add_runtime_dependency 'sprockets', '~> 3.0'
35
+
36
+ s.add_runtime_dependency 'thin'
37
+ s.add_runtime_dependency 'rack-livereload'
38
+ s.add_runtime_dependency 'guard-livereload'
39
+ s.add_runtime_dependency 'fog'
40
+
41
+ s.add_development_dependency 'pry'
42
+ s.add_development_dependency 'rspec', '~> 3.0'
43
+ s.add_development_dependency 'rspec-its'
44
+ s.add_development_dependency 'guard-rspec'
45
+ end
data/bin/antwort ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/antwort'
4
+
5
+ Antwort::CLI.start
data/lib/antwort.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'thor'
2
+ require 'dotenv'
3
+ Dotenv.load
4
+
5
+ # Namespace
6
+ module Antwort
7
+ end
8
+
9
+ require 'antwort/helpers'
10
+ require 'antwort/cli'
11
+ require 'antwort/builder'
12
+ require 'antwort/server'
13
+ require 'antwort/version'
@@ -0,0 +1,8 @@
1
+ require 'antwort/builder/helpers/logic'
2
+ require 'antwort/builder/helpers/sanitizers'
3
+ require 'antwort/builder/builder'
4
+ require 'antwort/builder/email'
5
+ require 'antwort/builder/partial'
6
+ require 'antwort/builder/flattener'
7
+ require 'antwort/builder/style'
8
+ require 'antwort/cli/helpers'
@@ -0,0 +1,104 @@
1
+ require 'fileutils'
2
+ require 'tilt'
3
+ require 'roadie'
4
+ require 'thor/shell'
5
+
6
+ module Antwort
7
+ class Builder
8
+ include Thor::Shell
9
+ include Antwort::Helpers
10
+ include Antwort::CLIHelpers
11
+ include Antwort::LogicHelpers
12
+ include Antwort::MarkupSanitizers
13
+
14
+ attr_reader :template_name, :build_id, :build_dir, :markup_dir, :source_dir, :scss_dir, :asset_server, :css, :css_style
15
+
16
+ def initialize(attrs = {})
17
+ attrs = symbolize_keys!(attrs)
18
+ @template_name = attrs[:email]
19
+ @build_id = attrs[:id]
20
+ @build_dir = "./build/#{template_name}-#{build_id}"
21
+ @markup_dir = "#{build_dir}/source"
22
+ @source_dir = "./emails/#{template_name}"
23
+ @scss_dir = "./assets/css/#{template_name}"
24
+ @css_style = attrs[:'css-style'].to_sym
25
+ @asset_server = ENV['ASSET_SERVER'] || '/assets'
26
+ post_initialize(attrs)
27
+ end
28
+
29
+ def post_initialize(*)
30
+ nil
31
+ end
32
+
33
+ def create_build_directories
34
+ return if Dir.exist? build_dir
35
+ Dir.mkdir "build" unless Dir.exist? "./build"
36
+ Dir.mkdir(build_dir)
37
+ Dir.mkdir("#{build_dir}/source")
38
+ end
39
+
40
+ def build
41
+ end
42
+
43
+ def load_css
44
+ css_file = "#{markup_dir}/inline.css"
45
+ build_css unless File.file? css_file
46
+ css = File.read(css_file)
47
+ css
48
+ end
49
+
50
+ def build_css
51
+ compile_scss(source: "#{scss_dir}/inline.scss", destination: "#{markup_dir}/inline.css")
52
+ compile_scss(source: "#{scss_dir}/include.scss", destination: "#{markup_dir}/include.css")
53
+ @css = load_css
54
+ end
55
+
56
+ def compile_scss(attrs = {})
57
+ source_file = attrs[:source]
58
+ destination_file = attrs[:destination]
59
+
60
+ if File.file? source_file
61
+ content = Tilt::ScssTemplate.new(source_file, style: @css_style).render
62
+ create_file(content: content, path: destination_file)
63
+ else
64
+ say 'Build failed. ', :red
65
+ say "#{source_file}.scss for #{template_name} not found."
66
+ end
67
+ end
68
+
69
+ def create_file(attrs)
70
+ content = attrs[:content]
71
+ path = attrs[:path]
72
+
73
+ file = File.new(path, 'w')
74
+ file.write(content)
75
+ file.close
76
+ say ' create ', :green
77
+ say path.gsub(/\A.\//, '')
78
+ file
79
+ end
80
+
81
+ def use_asset_server(markup = '')
82
+ replaced = "#{asset_server}/"
83
+ output = markup.gsub('/assets/', replaced)
84
+ output
85
+ end
86
+
87
+ def flatten_inlined_css(markup)
88
+ copy = ''
89
+
90
+ # loop through lines so we have the line number
91
+ markup.lines.each_with_index do |line, i|
92
+ f = Flattener.new(line).flatten
93
+ if f.flattened?
94
+ say " flattened CSS #{f.flattened_keys} ", :yellow
95
+ say "#{i}: #{f.source.strip}"
96
+ copy << f.flattened
97
+ else
98
+ copy << f.source
99
+ end
100
+ end
101
+ copy
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,61 @@
1
+ module Antwort
2
+ class EmailBuilder < Builder
3
+ attr_accessor :html_markup
4
+
5
+ def post_initialize(*)
6
+ app ||= Antwort::Server.new
7
+ mock ||= Rack::MockRequest.new(app)
8
+
9
+ request = mock.get("/template/#{template_name}")
10
+ if request.status == 200
11
+ create_build_directories
12
+ @html_markup = remove_livereload(request.body)
13
+ @inlined_file = "#{build_dir}/#{template_name}.html"
14
+ else
15
+ say 'Error: ', :red
16
+ say "Template '#{template_name}' not found."
17
+ end
18
+ end
19
+
20
+ def build
21
+ unless html_markup.nil?
22
+ build_css
23
+ build_html
24
+ inline_css
25
+ end
26
+
27
+ until File.exist?(@inlined_file)
28
+ sleep 1
29
+ end
30
+ return true
31
+ end
32
+
33
+ def build_html
34
+ markup = html_markup
35
+ markup = markup.gsub("/assets/#{template_name}/inline.css", 'inline.css')
36
+ .gsub("/assets/#{template_name}/include.css", 'include.css')
37
+ create_file(content: markup, path: "#{markup_dir}/#{template_name}.html")
38
+ end
39
+
40
+ def inline_css
41
+ markup = preserve_nbsps(html_markup)
42
+ document = Roadie::Document.new(markup)
43
+
44
+ document.asset_providers << Roadie::NullProvider.new
45
+ document.add_css(css)
46
+
47
+ inlined = restore_nbsps(document.transform)
48
+ inlined = cleanup_markup(inlined)
49
+ lnlined = remove_roadie_flags(inlined)
50
+ inlined = remove_excessive_newlines(inlined)
51
+ inlined = flatten_inlined_css(inlined)
52
+ create_file(content: inlined, path: @inlined_file)
53
+ end
54
+
55
+ def cleanup_markup(markup)
56
+ content = use_asset_server(markup)
57
+ content = add_included_css(content)
58
+ content
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ module Antwort
2
+ class Flattener
3
+
4
+ attr_reader :source, :styles, :flattened, :flattened_keys
5
+
6
+ def initialize(source='')
7
+ @source = source
8
+ @styles = find_styles
9
+ @flattened_keys = []
10
+ self
11
+ end
12
+
13
+ def flatten
14
+ copy = String.new(@source)
15
+ @styles.each do |m|
16
+ style = Antwort::Style.new(m)
17
+ @flattened_keys.concat(style.duplicate_keys) if style.duplicates?
18
+ copy.sub!(m, style.flattened_str) # flattened_str removes extra spaces and trailing semicolons
19
+ end
20
+
21
+ @flattened = copy
22
+ self
23
+ end
24
+
25
+ def flattened?
26
+ @flattened_keys.length > 0
27
+ end
28
+
29
+ private
30
+
31
+ def find_styles
32
+ a = []
33
+ @source.scan(/style="(.+?)"/).each { |match| a.push(match.first) } # first is regex group `(.+?)`
34
+ a
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,82 @@
1
+ module Antwort
2
+ module LogicHelpers
3
+
4
+ def preserve_nbsps(html = '')
5
+ html.gsub(/&nbsp;/, '%nbspace%')
6
+ end
7
+
8
+ def restore_nbsps(html = '')
9
+ html.gsub(/%nbspace%/, '&nbsp;')
10
+ end
11
+
12
+ def preserve_logic(html = '')
13
+ html = preserve_comments(html)
14
+ html = preserve_conditionals(html) # conditionals before loops, in case we have them inside loops
15
+ html = preserve_loops(html)
16
+ html = preserve_variables(html)
17
+ html = preserve_assignments(html)
18
+ html = convert_partials_to_includes(html)
19
+ html = convert_helper_wrappers(html)
20
+ html = preserve_leftover_statements(html) # must be last
21
+ html
22
+ end
23
+
24
+ def preserve_conditionals(html = '')
25
+ html.gsub(%r{<%\s*if (.*?)%>}, '{% if \1 %}') # if
26
+ .gsub(%r{<%\s*unless (.*?)%>}, '{% if !( \1) %}') # unless
27
+ .gsub(%r{<%\s*elsif(.*?)%>}, '{% elseif\1%}') # elsif
28
+ .gsub(%r{<%\s*else\s*%>}, '{% else %}') # else
29
+ .gsub(%r{<%\s*end\s*%>}, '{% end %}') # end
30
+ .gsub(/[ \t]{2,}%}/, ' %}') # remove extra white space, e.g. {% else %}
31
+ end
32
+
33
+ def preserve_loops(html = '')
34
+ html.gsub(%r{<%\s+(.*)(\.each\s+do\s+\|)\s*(\S+)\s*(\|\s+)%>}, '{% for \3 in \1 %}')
35
+ .gsub(%r{<%\s+(.*)(\.each_with_index\s+do\s+\|)\s*(\S+)\s*,\s*(\S+)\s*(\|\s+)%>}, '{% for \3 in \1 with: {@index: \4} %}')
36
+ .gsub(%r{<%\s*end\s*%>}, '{% end %}')
37
+ end
38
+
39
+ def preserve_comments(html = '')
40
+ html.gsub(%r{<%[ \t]*#(.*)%>},'{#\1#}')
41
+ .gsub(/{#([^=\s])/, '{# \1') # {#foo #} ->{# foo #}
42
+ end
43
+
44
+ def preserve_variables(html = '')
45
+ html.gsub(/\[:(.*?)\]/, '.\1') # a[:b][:c] -> a.b.c
46
+ .gsub(/\['(.*?)'\]/, '.\1') # a['b'] -> a.b
47
+ .gsub(/\["(.*?)"\]/, '.\1') # a["b"] -> a.b
48
+ .gsub(%r{<%=(.*?)%>}, '{{\1}}') # assume leftover erb output are variables
49
+ end
50
+
51
+ def preserve_assignments(html = '')
52
+ html.gsub(%r{<%\s+([A-Za-z0-9_]+)\s*(\|\|=)\s*(.*)\s+%>}, '{% set \1 = \1 || \3 %}')
53
+ .gsub(%r{<%\s+([A-Za-z0-9_]+)\s*(=)\s*(.*)\s+%>}, '{% set \1 = \3 %}')
54
+ end
55
+
56
+ def preserve_leftover_statements(html = '')
57
+ html.gsub(%r{<%\s*(.*?)?%>}, '{% \1%}') # no trailing space because group captures it
58
+ end
59
+
60
+ def restore_variables_in_links(html = '')
61
+ html.gsub('%7B%7B%20','{{ ')
62
+ .gsub('%20%7D%7D',' }}')
63
+ end
64
+
65
+ def convert_partials_to_includes(html = '')
66
+ html.gsub(%r{{{ partial :(.+?) }}}, '{% include \1 %}')
67
+ .gsub(%r{{% include (.+),\s+locals:(.+?)%}}, '{% include \1 with:\2%}')
68
+ .gsub(%r{(<%=\s+?partial\s+?:)(.+?),(.+?)(locals:)((.|\n)+?)(%>)}, '{% include \2 with:\5%}') # multiline partials not caught in presumably leftover variable
69
+ end
70
+
71
+ def convert_helper_wrappers(html = '')
72
+ html.gsub(%r{{{ button(.+?)}}}, '{% button\1%}')
73
+ .gsub(%r{{{ image_tag(.+?)}}}, '{% image_tag\1%}')
74
+ end
75
+
76
+ def cleanup_logic(html = '')
77
+ html.gsub(%r{({%|{{)(.*?)(&lt;)(.*?)(}}|%})}, '\1\2<\4\5') # <
78
+ .gsub(%r{({%|{{)(.*?)(&gt;)(.*?)(}}|%})}, '\1\2>\4\5') # >
79
+ .gsub(/&amp;&amp;/, '&&')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,29 @@
1
+ module Antwort
2
+ module MarkupSanitizers
3
+
4
+ def remove_livereload(markup = '')
5
+ markup.gsub(/<script.*?>(\s|\S)*<\/script>/i, '')
6
+ .gsub(/(<head.*?>\n)(\s*\n)*/i, '\1')
7
+ end
8
+
9
+ def add_included_css(markup = '')
10
+ css = File.read("#{markup_dir}/include.css")
11
+ css_markup = "</title>\n<style type=\"text/css\">\n" + css + "</style>\n"
12
+ markup.gsub(%r{</title>}i, css_markup)
13
+ end
14
+
15
+ def remove_roadie_flags(markup = '')
16
+ markup.gsub(' data-roadie-ignore', '')
17
+ end
18
+
19
+ def remove_excessive_newlines(markup = '')
20
+ markup.gsub(/^([ \t]*\n){3,}/m, "\n\n")
21
+ end
22
+
23
+ def remove_extra_dom(html = '')
24
+ html.gsub(/\<!(.*)\<body.*?\>/im, '')
25
+ .gsub(%r{</body>.*?</html>}im, '')
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ module Antwort
2
+ class PartialBuilder < Builder
3
+ attr_reader :templates
4
+
5
+ def post_initialize(*)
6
+ @templates = list_partials(source_dir).push('index.html.erb')
7
+ if templates.length < 1
8
+ say 'Error: ', :red
9
+ puts "No partials found in #{template_name} folder."
10
+ return
11
+ else
12
+ create_build_directories
13
+ end
14
+ end
15
+
16
+ def build
17
+ @css = load_css
18
+ templates.each { |t| build_html t }
19
+ end
20
+
21
+ def build_html(partial_name)
22
+ source_file = "#{source_dir}/#{partial_name}"
23
+ source = File.read(source_file)
24
+ markup = preserve_logic(source)
25
+ markup = preserve_nbsps(markup)
26
+ markup = preserve_operators_from_nokogiri(markup)
27
+ inlined = inline(markup)
28
+ inlined = restore_nbsps(inlined)
29
+ inlined = flatten_inlined_css(inlined)
30
+ filename = adjust_filename(partial_name)
31
+ create_file(content: inlined, path: "#{build_dir}/#{filename}")
32
+ end
33
+
34
+ def inline(markup)
35
+ # Force a complete DOM tree before inlining for nokogiri
36
+ # Otherwise we have random <p> at beginning of document
37
+ html = add_nokogiri_wrapper(markup)
38
+
39
+ document = Roadie::Document.new html
40
+ document.add_css(css)
41
+ inlined = document.transform
42
+ inlined = cleanup(inlined)
43
+
44
+ remove_nokogiri_wrapper(inlined)
45
+ end
46
+
47
+ def adjust_filename(filename)
48
+ filename = @template_name if filename == 'index.html.erb'
49
+
50
+ name = filename.gsub('.erb', '')
51
+ name << '.html' unless name[-5, 5] == '.html'
52
+ name = '_' << name unless name[0] == '_'
53
+ name
54
+ end
55
+
56
+ def cleanup(html = '')
57
+ code = remove_extra_dom(html)
58
+ code = cleanup_logic(code)
59
+ code = restore_variables_in_links(code)
60
+ code
61
+ end
62
+
63
+ private
64
+
65
+ def add_nokogiri_wrapper(html = '')
66
+ html = '<div id="valid-dom-tree">' + html + '</div><!--/#valid-dom-tree-->'
67
+ html
68
+ end
69
+
70
+ def remove_nokogiri_wrapper(html = '')
71
+ html.gsub('<div id="valid-dom-tree">', '')
72
+ .gsub(%r{</div>(\s*)<!--/#valid-dom-tree-->}, '')
73
+ end
74
+
75
+ def preserve_operators_from_nokogiri(html = '')
76
+ html.gsub(%r{{%\s*if(.*?)<(.*?)%}}, '{%\1&lt;\2%}')
77
+ .gsub(%r{{%\s*if(.*?)>(.*?)%}}, '{%\1&gt;\2%}')
78
+ end
79
+ end
80
+ end