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,59 @@
1
+ module Antwort
2
+ class Style
3
+ attr_reader :keys, :duplicate_keys, :flattened, :original
4
+
5
+ def initialize(style = '')
6
+ @style = style
7
+ @keys = []
8
+ @duplicate_keys = []
9
+ @flattened = []
10
+ @original = []
11
+
12
+ convert_to_hash
13
+
14
+ self
15
+ end
16
+
17
+ def original_str
18
+ @style
19
+ end
20
+
21
+ def flattened_str
22
+ hash_to_str @flattened
23
+ end
24
+
25
+ def duplicates?
26
+ @duplicate_keys.length > 0
27
+ end
28
+
29
+ private
30
+
31
+ def convert_to_hash
32
+ str = String.new(@style)
33
+ h = Hash.new
34
+ keys = Array.new
35
+
36
+ str.split(';').each do |s|
37
+ key = s.split(':').first.strip
38
+ val = s.split(':').last.strip
39
+ h[key] = val
40
+ @original << { key => val }
41
+
42
+ if @keys.include? key
43
+ @duplicate_keys << key
44
+ else
45
+ @keys << key
46
+ end
47
+ end
48
+ @flattened = h
49
+ end
50
+
51
+ # convert our flatted styles hash back into a string
52
+ def hash_to_str(hash)
53
+ str = ''
54
+ hash.each { |k,v| str << "#{k}:#{v};" }
55
+ str.chop! if str[-1] == ';' # remove trailing ';'
56
+ str
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ require 'fileutils'
2
+ require 'thor'
3
+
4
+ require 'antwort/cli/helpers'
5
+ require 'antwort/cli/cli'
6
+ require 'antwort/cli/send'
7
+ require 'antwort/cli/upload'
@@ -0,0 +1,275 @@
1
+ module Antwort
2
+ class CLI < Thor
3
+ include Thor::Actions
4
+ include Antwort::CLIHelpers
5
+
6
+ class_option :version, type: :boolean
7
+
8
+ # set template source path for Thor
9
+ def self.source_root
10
+ File.expand_path('../../../template', File.dirname(__FILE__))
11
+ end
12
+
13
+ #-- init
14
+
15
+ desc 'init [project_name]', 'Initializes a new Antwort Email project'
16
+ method_option :user,
17
+ type: :string,
18
+ desc: 'Your username to antwort gem server'
19
+ method_option :key,
20
+ type: :string,
21
+ desc: 'Your password to antwort gem server'
22
+ method_option :git,
23
+ type: :boolean,
24
+ default: true,
25
+ desc: 'Initializes git repo if set to true'
26
+ method_option :bundle,
27
+ type: :boolean,
28
+ default: true,
29
+ desc: 'Runs bundle command in new repo'
30
+
31
+ def init(project_name)
32
+ @project_name = project_name
33
+ @user = options[:user] if options[:user]
34
+ @key = options[:key] if options[:key]
35
+
36
+ copy_project
37
+ initialize_git_repo if options[:git]
38
+ run_bundler if options[:bundle]
39
+ say "New project initialized in: ./#{project_directory}/", :green
40
+ end
41
+
42
+ #-- new
43
+
44
+ desc 'new [email_id]', 'Creates a new email template'
45
+ method_option aliases: 'n'
46
+ def new(email_id)
47
+ @email_id = email_id
48
+ copy_email
49
+ end
50
+
51
+ #-- list
52
+
53
+ desc 'list', 'Lists all emails in the ./emails directory by id'
54
+ method_option aliases: 'l'
55
+ def list
56
+ list_folders('./emails').each { |e| puts "- #{e}" }
57
+ end
58
+
59
+ #-- upload
60
+
61
+ desc 'upload', 'Uploads email assets to AWS S3'
62
+ method_option :force,
63
+ type: :boolean,
64
+ default: false,
65
+ aliases: '-f',
66
+ desc: 'Overwrites existing files on the server'
67
+ method_option aliases: 'u'
68
+ def upload(email_id)
69
+ Upload.new(email_id, options[:force]).upload
70
+ end
71
+
72
+ #-- send
73
+
74
+ desc 'send [email_id]', 'Sends built email via SMTP'
75
+ method_option :from,
76
+ type: :string,
77
+ default: ENV['SEND_FROM'],
78
+ aliases: '-f',
79
+ desc: 'Email address of sender'
80
+ method_option :recipient,
81
+ type: :string,
82
+ default: ENV['SEND_TO'],
83
+ aliases: '-r',
84
+ desc: 'Email address of receipient'
85
+ method_option :subject,
86
+ type: :string,
87
+ aliases: '-s',
88
+ desc: 'Email Subject. Defaults to <title> value if blank.'
89
+ def send(email_id)
90
+ build = last_build_by_id(email_id)
91
+
92
+ if build.nil?
93
+ say " warning ", :yellow
94
+ say "No build found for '#{email_id}'. Building now..."
95
+ build(email_id)
96
+ build = last_build_by_id(email_id)
97
+ end
98
+
99
+ Send.new(build, options).send
100
+ end
101
+
102
+ #-- server
103
+
104
+ desc 'server', 'Starts http://localhost:9292 server for coding and previewing emails'
105
+ method_option :port,
106
+ type: :string,
107
+ default: 9292,
108
+ aliases: '-p',
109
+ desc: 'Port number of server'
110
+ def server
111
+ require 'antwort'
112
+ Antwort::Server.run!(port: options[:port])
113
+ end
114
+
115
+ #-- build
116
+
117
+ desc 'build [email_id]', 'Builds email markup and inlines CSS from source'
118
+ method_option aliases: 'b'
119
+ method_option :all,
120
+ type: :boolean,
121
+ default: false,
122
+ aliases: '-a',
123
+ desc: 'Build all templates'
124
+ method_option :partials,
125
+ type: :boolean,
126
+ default: false,
127
+ aliases: '-p',
128
+ desc: 'Build partials'
129
+ method_option :'css-style',
130
+ type: :string,
131
+ default: 'expanded',
132
+ aliases: '-c',
133
+ desc: 'CSS output style'
134
+ def build(email_id='')
135
+ require 'antwort'
136
+
137
+ emails = options[:all] ? available_emails : Array.new.push(email_id)
138
+
139
+ emails.each do |email_id|
140
+ attrs = { email: email_id, id: create_id_from_timestamp }.merge(options)
141
+ email = Antwort::EmailBuilder.new(attrs)
142
+ until email.build
143
+ sleep 1
144
+ end
145
+
146
+ if build_partials?
147
+ partials = Antwort::PartialBuilder.new(attrs)
148
+ sleep 1 until partials.build
149
+ end
150
+ end
151
+
152
+ show_accuracy_warning if build_partials?
153
+
154
+ return true
155
+ end
156
+
157
+ #-- prune
158
+
159
+ desc 'prune', 'Removes all files in the ./build directory'
160
+ method_option :force,
161
+ type: :boolean,
162
+ default: false,
163
+ aliases: '-f',
164
+ desc: 'Removes all files in the ./build directory'
165
+ def prune
166
+ if confirms_prune?
167
+ build_dir = File.expand_path('./build')
168
+ Dir.foreach(build_dir) do |f|
169
+ next if f.to_s[0] == '.'
170
+ say " delete ", :red
171
+ say "./build/#{f}/"
172
+ FileUtils.rm_rf(File.expand_path("./build/#{f}"))
173
+ end
174
+ else
175
+ say "prune aborted."
176
+ end
177
+ end
178
+
179
+ #-- remove
180
+
181
+ desc 'remove [email_id]', 'Removes an email, incl. its assets, styles and data'
182
+ method_option :force,
183
+ type: :boolean,
184
+ default: false,
185
+ aliases: '-f',
186
+ desc: 'Removes an email, incl. its assets, styles and data'
187
+ def remove(email_id)
188
+ @email_id = email_id
189
+ if confirms_remove?
190
+ remove_email
191
+ else
192
+ say "Remove aborted."
193
+ end
194
+ end
195
+
196
+ #-- version
197
+
198
+ desc 'version','Ouputs version number'
199
+ def version
200
+ puts "Version: #{Antwort::VERSION}" if options[:version]
201
+ end
202
+
203
+ default_task :version
204
+
205
+ attr_reader :project_name, :email_id
206
+
207
+ no_commands do
208
+
209
+ def build_partials?
210
+ options[:partials]
211
+ end
212
+
213
+ def confirms_prune?
214
+ options[:force] || yes?('Are you sure you want to delete all folders in the ./build directory? (y/n)')
215
+ end
216
+
217
+ def confirms_remove?
218
+ options[:force] || yes?("Are you sure you want to delete '#{email_id}', including its css, images and data? (y/n)")
219
+ end
220
+
221
+ def copy_email
222
+ directory 'email/css',
223
+ File.join('assets', 'css', email_directory)
224
+ directory 'email/images',
225
+ File.join('assets', 'images', email_directory)
226
+ create_file File.join('data', "#{email_id}.yml")
227
+ copy_file 'email/email.html.erb',
228
+ File.join('emails', email_directory, 'index.html.erb')
229
+ end
230
+
231
+ def remove_email
232
+ remove_file File.expand_path("./data/#{email_id}.yml")
233
+ remove_dir File.expand_path("./assets/css/#{email_id}/")
234
+ remove_dir File.expand_path("./assets/images/#{email_id}/")
235
+ remove_dir File.expand_path("./emails/#{email_id}/")
236
+ end
237
+
238
+ def copy_project
239
+ directory 'project', project_directory
240
+ end
241
+
242
+ def initialize_git_repo
243
+ inside(project_directory) do
244
+ run('git init .')
245
+ end
246
+ end
247
+
248
+ def run_bundler
249
+ inside(project_directory) do
250
+ run('bundle')
251
+ end
252
+ end
253
+
254
+ def email_directory
255
+ email_id.downcase.gsub(/([^A-Za-z0-9_\/-]+)|(--)/, '')
256
+ end
257
+
258
+ def project_directory
259
+ project_name.downcase.gsub(/([^A-Za-z0-9_\/-]+)|(--)/, '')
260
+ end
261
+
262
+ def create_id_from_timestamp
263
+ stamp = Time.now.to_s
264
+ stamp.split(' ')[0..1].join.gsub(/(-|:)/, '')
265
+ end
266
+
267
+ def show_accuracy_warning
268
+ say ''
269
+ say '** NOTE: Accuracy of Inlinied Partials **', :yellow
270
+ say 'Partials do not have access to the full DOM tree. Therefore, nested CSS selectors, e.g. ".layout td",'
271
+ say 'may not be matched for inlining. Always double check your code before use in production!'
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,44 @@
1
+ module Antwort
2
+ module CLIHelpers
3
+
4
+ def built_emails
5
+ list_folders('./build')
6
+ end
7
+
8
+ def available_emails
9
+ list_folders('./emails')
10
+ end
11
+
12
+ def images_dir(email_id)
13
+ "./assets/images/#{email_id}"
14
+ end
15
+
16
+ def count_files(dir)
17
+ Dir[File.join(dir, '**', '*')].count { |f| File.file? f }
18
+ end
19
+
20
+ def last_build_by_id(email_id)
21
+ built_emails.select { |f| f.split('-')[0..-2].join('-') == email_id }.sort.last
22
+ end
23
+
24
+ def list_folders(folder_name)
25
+ path = File.expand_path(folder_name)
26
+ Dir.entries(path).select { |f| !f.include? '.' }
27
+ end
28
+
29
+ def list_partials(folder_name)
30
+ path = File.expand_path(folder_name)
31
+ Dir.entries(path).select { |f| f[0]== '_' && f[-4,4] == '.erb' }
32
+ end
33
+
34
+ def folder_exists?(folder_name)
35
+ if Dir.exists?(folder_name)
36
+ return true
37
+ else
38
+ say "Error: Folder #{folder_name} does not exist.", :red
39
+ return false
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ require 'thor/shell'
2
+ require 'mail'
3
+
4
+ module Antwort
5
+ class CLI
6
+ class Send
7
+ include Thor::Shell
8
+ attr_reader :build_id, :sender, :recipient, :subject
9
+
10
+ Mail.defaults do
11
+ delivery_method :smtp,
12
+ address: ENV['SMTP_SERVER'],
13
+ port: ENV['SMTP_PORT'],
14
+ user_name: ENV['SMTP_USERNAME'],
15
+ password: ENV['SMTP_PASSWORD'],
16
+ authentication: 'plain',
17
+ enable_starttls_auto: false,
18
+ return_response: true
19
+ end
20
+
21
+ def initialize(build_id, options={})
22
+ @build_id = build_id
23
+ @html_body = File.open("#{build_folder}/#{template_name}.html").read
24
+
25
+ @recipient = options[:recipient]
26
+ @sender = options[:from] || ENV['SEND_FROM']
27
+ @subject = options[:subject] || "[Test] " << extract_title(@html_body)
28
+ end
29
+
30
+ def send
31
+ # because scope changes inside mail DSL
32
+ mail_from = @sender
33
+ mail_to = @recipient
34
+ mail_subject = @subject
35
+
36
+ # setup email
37
+ mail = Mail.new do
38
+ from mail_from
39
+ to mail_to
40
+ subject mail_subject
41
+
42
+ text_part do
43
+ body 'This is plain text'
44
+ end
45
+
46
+ html_part do
47
+ content_type 'text/html; charset=UTF-8'
48
+ body @html_body
49
+ end
50
+ end
51
+
52
+ # send email
53
+ if mail.deliver!
54
+ say "Sent Email \"#{template_name}\" at #{Time.now.strftime('%d.%m.%Y %H:%M')}", :green
55
+ say " to: #{@recipient}"
56
+ say " subject: #{@subject}"
57
+ say " html: #{build_id}/#{template_name}.html"
58
+ else
59
+ say "Error sending #{build_id}/#{template_name} at #{Time.now.strftime('%d.%m.%Y %H:%M')}", :red
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def template_name
66
+ @build_id.split('-')[0...-1].join('-') # removes timestamp ID
67
+ end
68
+
69
+ def build_folder
70
+ "build/#{@build_id}"
71
+ end
72
+
73
+ def extract_title(body = '')
74
+ body.scan(%r{<title>(.*?)</title>}).first.first
75
+ end
76
+
77
+ end
78
+ end
79
+ end