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
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe Antwort::PartialBuilder do
4
+
5
+ before :all do
6
+ Dir.chdir(fixtures_root)
7
+ end
8
+
9
+ before :each do
10
+ allow($stdout).to receive(:write) # Ignore warnings
11
+ @builder = Antwort::PartialBuilder.new({email: '1-demo', id: '123', 'css-style': 'expanded'})
12
+ end
13
+
14
+ describe "Builds" do
15
+ it "builds HTML"
16
+ it "inlines CSS"
17
+ end
18
+
19
+ describe "Helpers" do
20
+ describe "adjusts filename as necssary (make sure it ends with .html)" do
21
+
22
+ it "always starts with _" do
23
+ expect(@builder.adjust_filename('foo.erb')).to eq('_foo.html')
24
+ expect(@builder.adjust_filename('foo.html')).to eq('_foo.html')
25
+ expect(@builder.adjust_filename('foo.html.erb')).to eq('_foo.html')
26
+ end
27
+
28
+ it "always ends with .html" do
29
+ expect(@builder.adjust_filename('_foo.html.erb')).to eq('_foo.html')
30
+ expect(@builder.adjust_filename('_foo.erb')).to eq('_foo.html')
31
+ expect(@builder.adjust_filename('_foo.html')).to eq('_foo.html')
32
+ end
33
+
34
+ it "uses template name instead of _index" do
35
+ expect(@builder.adjust_filename('index.html.erb')).to eq('_1-demo.html')
36
+ end
37
+ end
38
+
39
+ describe "save from Nokogiri" do
40
+ let (:start) { '<div id="valid-dom-tree">' }
41
+ let (:ende) {'</div><!--/#valid-dom-tree-->' }
42
+
43
+ it "can add nokogiri wrapper" do
44
+ expect(@builder.send(:add_nokogiri_wrapper, 'foo')).to eq(start + 'foo' + ende)
45
+ end
46
+
47
+ it "can remove nokogiri wrapper" do
48
+ expect(@builder.send(:remove_nokogiri_wrapper, start + 'foo' + ende)).to eq('foo')
49
+ expect(@builder.send(:remove_nokogiri_wrapper, start + 'foo' + "</div> <!--/#valid-dom-tree-->")).to eq('foo')
50
+ expect(@builder.send(:remove_nokogiri_wrapper, start + 'foo' + "</div>\n<!--/#valid-dom-tree-->")).to eq('foo')
51
+ end
52
+
53
+ # TODO: These specs don't work because PartialBuilder cannot find matching CSS
54
+
55
+ it "adds wrapper before inlining", skip: true do
56
+ @builder.inline('foo')
57
+ expect(@builder).to receive(:add_nokogiri_wrapper)
58
+ end
59
+
60
+ it "removes wrapper after inlining", skip: true do
61
+ @builder.inline('foo')
62
+ expect(@builder).to receive(:remove_nokogiri_wrapper)
63
+ end
64
+ end
65
+
66
+ describe "Clean up" do
67
+ it "can convert logic html entities back to operators" do
68
+ h = {
69
+ "{% &lt;= %}" => "{% <= %}", # only convert operators within logic
70
+ "{{ &lt;= }}" => "{{ <= }}", # leave operators in content
71
+ "{% &gt;= %}" => "{% >= %}",
72
+ "{{ &gt;= }}" => "{{ >= }}",
73
+ " &amp;&amp; " => " && "
74
+ }
75
+ h.each do |key, value|
76
+ expect(@builder.cleanup_logic(key)).to eq(value)
77
+ end
78
+ end
79
+
80
+ it "can restore variables in links" do
81
+ a = '<a href="%7B%7B%20hello%20%7D%7D">world</a>'
82
+ b = '<a href="{{ hello }}">world</a>'
83
+ expect(@builder.restore_variables_in_links(a)).to eq(b)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe Antwort::Style do
4
+
5
+ describe "on Initialize" do
6
+ let (:style) { Antwort::Style.new('font-weight:bold;font-size:24px;line-height:28px;font-family:Helvetica, Arial, sans-serif;color:#111111;color:#2CA4D8') }
7
+
8
+ it "remembers all keys" do
9
+ expect(style.keys).to eq(['font-weight','font-size','line-height','font-family','color'])
10
+ end
11
+ it "remembers duplicate keys" do
12
+ expect(style.duplicate_keys).to eq(['color'])
13
+ end
14
+ end
15
+
16
+ describe "Readable Attributes" do
17
+ let (:style) { Antwort::Style.new('font-size:24px;color:black;color:red') }
18
+
19
+ context "not flattened" do
20
+ it "can be accessed as a hash" do
21
+ expect(style.original).to eq([{'font-size' => '24px'}, {'color'=>'black'}, {'color'=>'red'}])
22
+ end
23
+ it "can be accessed as a string" do
24
+ expect(style.original_str).to eq('font-size:24px;color:black;color:red')
25
+ end
26
+ end
27
+
28
+ context "flattened" do
29
+ it "can be accessed as a hash" do
30
+ expect(style.flattened).to eq({
31
+ 'font-size' => '24px',
32
+ 'color' => 'red'
33
+ })
34
+ end
35
+ it "can be accessed as a string" do
36
+ expect(style.flattened_str).to eq('font-size:24px;color:red')
37
+ end
38
+ end
39
+
40
+ it "has a duplicates? method" do
41
+ expect(Antwort::Style.new('color:red;color:black').duplicates?).to eq(true)
42
+ expect(Antwort::Style.new('color:red;font-size:12px').duplicates?).to eq(false)
43
+ end
44
+ end
45
+
46
+ describe "Code Formatting" do
47
+ it "trims leading and trailing white space from styles" do
48
+ style = Antwort::Style.new('color:red; color: black;border :none;')
49
+ expect(style.flattened).to eq({
50
+ 'color' => 'black',
51
+ 'border' => 'none'})
52
+ expect(style.flattened).not_to eq({
53
+ ' color' => ' black',
54
+ 'border ' => 'none'})
55
+ end
56
+ end
57
+
58
+ describe "Internal Helpers" do
59
+ it "can push styles hash back into sting" do
60
+ hash = { 'font-size' => '14px', 'font-family' => 'Helvetica' }
61
+ style = Antwort::Style.new
62
+ expect(style.send(:hash_to_str, hash)).to eq('font-size:14px;font-family:Helvetica')
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ class Dummy
4
+ end
5
+
6
+ describe Antwort::CLIHelpers do
7
+
8
+ before :each do
9
+ @helper = Dummy.new
10
+ @helper.extend(Antwort::CLIHelpers)
11
+ end
12
+
13
+ before :all do
14
+ Dir.chdir(fixtures_root)
15
+ end
16
+
17
+ describe 'CLI Helpers' do
18
+
19
+ it '#built_emails' do
20
+ b = @helper.built_emails
21
+ expect(b).to include('demo-123456')
22
+ expect(b).not_to include('2-no-layout')
23
+ end
24
+
25
+ it '#available_emails - lists all available emails' do
26
+ a = @helper.available_emails
27
+ expect(a).to include('1-demo')
28
+ expect(a).to include('2-no-layout')
29
+ expect(a).to include('3-no-title')
30
+ end
31
+
32
+ it '#images_dir - returns images asset path by EMAIL_ID' do
33
+ expect(@helper.images_dir('foo')).to eq './assets/images/foo'
34
+ end
35
+
36
+ it '#count_files' do
37
+ expect(@helper.count_files './assets/images/shared').to be(2)
38
+ expect(@helper.count_files './assets/images/1-demo').to be(1)
39
+ end
40
+
41
+ it '#last_build_by_id' do
42
+ expect(@helper.last_build_by_id 'foo').to eq('foo-1')
43
+ expect(@helper.last_build_by_id 'demo').to eq('demo-123457')
44
+ expect(@helper.last_build_by_id 'demo').not_to eq('demo-123456')
45
+ expect(@helper.last_build_by_id 'demo').not_to eq('demo-bar-123')
46
+ end
47
+
48
+ it '#list_folders' do
49
+ a = @helper.list_folders '.'
50
+ expect(a).to include('assets')
51
+ expect(a).to include('build')
52
+ expect(a).to include('data')
53
+ expect(a).to include('emails')
54
+ expect(a).to include('views')
55
+ expect(a).not_to include('support')
56
+ expect(a).not_to include('lib')
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe Antwort::CLI::Upload do
4
+
5
+ subject { described_class.new('newsletter') }
6
+
7
+ before :all do
8
+ Dir.chdir(fixtures_root)
9
+ end
10
+
11
+ before(:each) do
12
+ allow($stdout).to receive(:write)
13
+ Fog.mock!
14
+ allow_any_instance_of(Thor::Actions).to receive(:yes?).and_return(true)
15
+ end
16
+
17
+ after(:each) { Fog.unmock! }
18
+
19
+ describe '#upload' do
20
+ before :each do
21
+ allow(Dir).to receive(:foreach)
22
+ end
23
+
24
+ it 'cleans S3 directory' do
25
+ expect(subject).to receive(:clean_directory!)
26
+ subject.upload
27
+ end
28
+ end
29
+
30
+ describe '#connection' do
31
+ it 'returns S3 connection' do
32
+ expect(subject.connection).to be_a(Fog::Storage::AWS::Mock)
33
+ end
34
+ end
35
+
36
+ describe '#directory' do
37
+ it 'responds to #directory' do
38
+ expect(subject).to respond_to(:directory)
39
+ end
40
+ end
41
+
42
+ describe '#clean_directory!' do
43
+ it 'responds to #clean_directory!' do
44
+ expect(subject).to respond_to(:clean_directory!)
45
+ end
46
+ end
47
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Antwort::CLI do
4
+
5
+ before :all do
6
+ Dir.chdir(fixtures_root)
7
+ end
8
+
9
+ describe '#upload' do
10
+ let(:output) { capture(:stdout) { subject.upload('newsletter') } }
11
+
12
+ before :each do
13
+ allow($stdout).to receive(:write)
14
+ end
15
+
16
+ context 'user confirms upload' do
17
+ before :each do
18
+ allow_any_instance_of(Thor::Actions).to receive(:yes?).and_return(true)
19
+ end
20
+
21
+ it 'uploads mail' do
22
+ expect(subject).to receive(:upload)
23
+ output
24
+ end
25
+
26
+ it 'messages success' do
27
+ expect(output).to include('create')
28
+ end
29
+ end
30
+
31
+ context 'user denies upload' do
32
+ before :each do
33
+ allow_any_instance_of(Thor::Actions).to receive(:yes?).and_return(false)
34
+ end
35
+
36
+ it 'does not upload mail' do
37
+ expect(subject).not_to receive(:upload_mail)
38
+ output
39
+ end
40
+
41
+ it 'messages cancelation' do
42
+ expect(output).to include('aborted')
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head></head>
4
+ <body>
5
+ <h1>Build</h1>
6
+ </body>
7
+ </html>
@@ -0,0 +1,7 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head></head>
4
+ <body>
5
+ <h1>Build</h1>
6
+ </body>
7
+ </html>
@@ -0,0 +1,7 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head></head>
4
+ <body>
5
+ <h1>Build</h1>
6
+ </body>
7
+ </html>
@@ -0,0 +1,7 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head></head>
4
+ <body>
5
+ <h1>Build</h1>
6
+ </body>
7
+ </html>
@@ -0,0 +1,3 @@
1
+ foo:
2
+ - title: 'foo'
3
+ - title: 'bar'
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Email One
3
+ ---
4
+
5
+ <h1>Hello One</h1>
6
+
7
+ <% @foo.each do |f| %>
8
+ <%= f[:title] %>
9
+ <% end %>
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: Email Two
3
+ layout: false
4
+ ---
5
+
6
+ <h1>Hello Two</h1>
@@ -0,0 +1 @@
1
+ <h1>Hello Three</h1>
@@ -0,0 +1 @@
1
+ 404 error, template not found
@@ -0,0 +1,14 @@
1
+
2
+ <% if @pages.length == 0 %>
3
+ No emails found
4
+ <% else %>
5
+ <ul>
6
+ <% @pages.each do |page| %>
7
+ <li>
8
+ <a href="template/<%= page[:path] %>">
9
+ <%= page[:title] %>
10
+ </a>
11
+ </li>
12
+ <% end %>
13
+ </ul>
14
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title><%= @title || "Untitled" %></title>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <%= yield %>
4
+ </body>
5
+ </html>
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Antwort Server' do
4
+
5
+ def app
6
+ Antwort::Server
7
+ end
8
+
9
+ describe "Index page" do
10
+ it "is valid" do
11
+ get '/'
12
+ expect(last_response.status).to eq(200)
13
+ end
14
+
15
+ it "lists available templates by title, includes link" do
16
+ get '/'
17
+ expect(last_response.body).to include('Email One')
18
+ expect(last_response.body).to include('Email Two')
19
+ expect(last_response.body).to include('<a href="template/1-demo">')
20
+ expect(last_response.body).to include('<a href="template/2-no-layout">')
21
+ end
22
+ end
23
+
24
+ describe "Template show action" do
25
+
26
+ it "shows email preview" do
27
+ get '/template/1-demo'
28
+ expect(last_response.body).to include('<h1>Hello One</h1>')
29
+ expect(last_response.body).to include('<title>Email One</title>')
30
+ end
31
+
32
+ it "loads corresponding data yaml file" do
33
+ get '/template/1-demo'
34
+ expect(last_response.body).to include('foo')
35
+ expect(last_response.body).to include('bar')
36
+ end
37
+
38
+ it "respects layout:false metadata attribute" do
39
+ get '/template/2-no-layout'
40
+ expect(last_response.body).not_to include('<body>')
41
+ end
42
+
43
+ it "has default title if none found" do
44
+ get 'template/3-no-title'
45
+ expect(last_response.body).to include('Untitled')
46
+ end
47
+
48
+ it "shows 404 error if template not found" do
49
+ get '/template/does-not-exist'
50
+ expect(last_response.status).to eq(404)
51
+ end
52
+ end
53
+
54
+ end