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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +249 -0
- data/Gemfile +3 -0
- data/Guardfile +14 -0
- data/README.md +108 -0
- data/Rakefile +14 -0
- data/antwort.gemspec +45 -0
- data/bin/antwort +5 -0
- data/lib/antwort.rb +13 -0
- data/lib/antwort/builder.rb +8 -0
- data/lib/antwort/builder/builder.rb +104 -0
- data/lib/antwort/builder/email.rb +61 -0
- data/lib/antwort/builder/flattener.rb +37 -0
- data/lib/antwort/builder/helpers/logic.rb +82 -0
- data/lib/antwort/builder/helpers/sanitizers.rb +29 -0
- data/lib/antwort/builder/partial.rb +80 -0
- data/lib/antwort/builder/style.rb +59 -0
- data/lib/antwort/cli.rb +7 -0
- data/lib/antwort/cli/cli.rb +275 -0
- data/lib/antwort/cli/helpers.rb +44 -0
- data/lib/antwort/cli/send.rb +79 -0
- data/lib/antwort/cli/upload.rb +89 -0
- data/lib/antwort/helpers.rb +19 -0
- data/lib/antwort/server.rb +70 -0
- data/lib/antwort/server/assets.rb +23 -0
- data/lib/antwort/server/helpers.rb +67 -0
- data/lib/antwort/server/markup.rb +39 -0
- data/lib/antwort/version.rb +3 -0
- data/spec/builder/builder_spec.rb +30 -0
- data/spec/builder/email_spec.rb +21 -0
- data/spec/builder/flattener_spec.rb +64 -0
- data/spec/builder/helpers_logic_spec.rb +244 -0
- data/spec/builder/partial_spec.rb +87 -0
- data/spec/builder/style_spec.rb +66 -0
- data/spec/cli/helpers_spec.rb +60 -0
- data/spec/cli/upload_spec.rb +47 -0
- data/spec/cli_spec.rb +46 -0
- data/spec/fixtures/assets/images/1-demo/placeholder.png +0 -0
- data/spec/fixtures/assets/images/newsletter/placeholder.png +0 -0
- data/spec/fixtures/assets/images/shared/placeholder-grey.png +0 -0
- data/spec/fixtures/assets/images/shared/placeholder-white.png +0 -0
- data/spec/fixtures/build/demo-123456/build.html +7 -0
- data/spec/fixtures/build/demo-123457/build.html +7 -0
- data/spec/fixtures/build/demo-bar-123/build.html +7 -0
- data/spec/fixtures/build/foo-1/build.html +7 -0
- data/spec/fixtures/data/1-demo.yml +3 -0
- data/spec/fixtures/emails/1-demo/index.html.erb +9 -0
- data/spec/fixtures/emails/2-no-layout/index.html.erb +6 -0
- data/spec/fixtures/emails/3-no-title/index.html.erb +1 -0
- data/spec/fixtures/views/404.html.erb +1 -0
- data/spec/fixtures/views/index.html.erb +14 -0
- data/spec/fixtures/views/layout.erb +8 -0
- data/spec/fixtures/views/server.erb +5 -0
- data/spec/server_spec.rb +54 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/capture.rb +17 -0
- data/template/email/css/include.scss +5 -0
- data/template/email/css/inline.scss +5 -0
- data/template/email/email.html.erb +11 -0
- data/template/email/images/.empty_directory +0 -0
- data/template/project/.env.sample +21 -0
- data/template/project/.gitignore.tt +17 -0
- data/template/project/.ruby-version +1 -0
- data/template/project/Gemfile.tt +9 -0
- data/template/project/Guardfile +9 -0
- data/template/project/assets/css/demo/include.scss +3 -0
- data/template/project/assets/css/demo/inline.scss +33 -0
- data/template/project/assets/css/server.scss +167 -0
- data/template/project/assets/css/shared/_base.scss +64 -0
- data/template/project/assets/css/shared/_mixins.scss +25 -0
- data/template/project/assets/css/shared/_reset.scss +59 -0
- data/template/project/assets/css/shared/_vars.scss +12 -0
- data/template/project/assets/css/shared/include.scss +23 -0
- data/template/project/assets/css/shared/inline.scss +9 -0
- data/template/project/assets/images/.gitkeep +0 -0
- data/template/project/assets/images/shared/placeholder.png +0 -0
- data/template/project/build/.empty_directory +0 -0
- data/template/project/data/.empty_directory +0 -0
- data/template/project/data/config.yml +3 -0
- data/template/project/data/demo.yml +4 -0
- data/template/project/emails/demo/_partial.html.erb +9 -0
- data/template/project/emails/demo/index.html.erb +54 -0
- data/template/project/views/404.html.erb +8 -0
- data/template/project/views/index.html.erb +18 -0
- data/template/project/views/layout.erb +38 -0
- data/template/project/views/markup/_button.html.erb +5 -0
- data/template/project/views/markup/_image_tag.html.erb +10 -0
- data/template/project/views/server.erb +32 -0
- 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
|
data/lib/antwort/cli.rb
ADDED
@@ -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
|