antwort 0.0.12

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