inkcite 1.14.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/assets/init/config.yml +2 -0
  3. data/assets/social/facebook.png +0 -0
  4. data/assets/social/instagram.png +0 -0
  5. data/assets/social/pintrest.png +0 -0
  6. data/assets/social/twitter.png +0 -0
  7. data/inkcite.gemspec +2 -2
  8. data/lib/inkcite.rb +1 -0
  9. data/lib/inkcite/cli/base.rb +32 -6
  10. data/lib/inkcite/cli/preview.rb +24 -28
  11. data/lib/inkcite/cli/server.rb +22 -19
  12. data/lib/inkcite/cli/test.rb +1 -1
  13. data/lib/inkcite/email.rb +8 -4
  14. data/lib/inkcite/facade/animation.rb +4 -0
  15. data/lib/inkcite/facade/keyframe.rb +26 -3
  16. data/lib/inkcite/image/base.rb +38 -0
  17. data/lib/inkcite/image/guetzli_minifier.rb +62 -0
  18. data/lib/inkcite/image/image_minifier.rb +143 -0
  19. data/lib/inkcite/image/image_optim_minifier.rb +90 -0
  20. data/lib/inkcite/image/mozjpeg_minifier.rb +92 -0
  21. data/lib/inkcite/mailer.rb +201 -112
  22. data/lib/inkcite/minifier.rb +2 -146
  23. data/lib/inkcite/post_processor.rb +13 -0
  24. data/lib/inkcite/renderer.rb +19 -0
  25. data/lib/inkcite/renderer/background.rb +53 -14
  26. data/lib/inkcite/renderer/base.rb +29 -15
  27. data/lib/inkcite/renderer/button.rb +1 -1
  28. data/lib/inkcite/renderer/carousel.rb +245 -0
  29. data/lib/inkcite/renderer/container_base.rb +10 -0
  30. data/lib/inkcite/renderer/div.rb +1 -3
  31. data/lib/inkcite/renderer/fireworks.rb +54 -40
  32. data/lib/inkcite/renderer/footnote.rb +22 -2
  33. data/lib/inkcite/renderer/image.rb +11 -0
  34. data/lib/inkcite/renderer/image_base.rb +3 -6
  35. data/lib/inkcite/renderer/in_browser.rb +4 -0
  36. data/lib/inkcite/renderer/link.rb +39 -12
  37. data/lib/inkcite/renderer/mobile_image.rb +1 -1
  38. data/lib/inkcite/renderer/responsive.rb +9 -1
  39. data/lib/inkcite/renderer/social.rb +31 -3
  40. data/lib/inkcite/renderer/special_effect.rb +22 -13
  41. data/lib/inkcite/renderer/sup.rb +32 -0
  42. data/lib/inkcite/renderer/table_base.rb +3 -0
  43. data/lib/inkcite/renderer/topic.rb +76 -0
  44. data/lib/inkcite/renderer/trademark.rb +47 -0
  45. data/lib/inkcite/renderer/video_preview.rb +3 -2
  46. data/lib/inkcite/uploader.rb +2 -3
  47. data/lib/inkcite/util.rb +51 -0
  48. data/lib/inkcite/version.rb +1 -1
  49. data/lib/inkcite/view.rb +140 -54
  50. data/lib/inkcite/view/context.rb +1 -31
  51. data/lib/inkcite/view/media_query.rb +6 -0
  52. data/test/animation_spec.rb +7 -0
  53. data/test/parser_spec.rb +1 -1
  54. data/test/renderer/background_spec.rb +16 -12
  55. data/test/renderer/div_spec.rb +11 -0
  56. data/test/renderer/footnote_spec.rb +5 -1
  57. data/test/renderer/image_spec.rb +51 -28
  58. data/test/renderer/link_spec.rb +20 -8
  59. data/test/renderer/lorem_spec.rb +2 -2
  60. data/test/renderer/mobile_image_spec.rb +6 -0
  61. data/test/renderer/mobile_style_spec.rb +3 -3
  62. data/test/renderer/redacted_spec.rb +2 -2
  63. data/test/renderer/social_spec.rb +6 -6
  64. data/test/renderer/table_spec.rb +4 -0
  65. data/test/renderer/topic_spec.rb +28 -0
  66. data/test/renderer/trademark_spec.rb +40 -0
  67. data/test/renderer/video_preview_spec.rb +1 -1
  68. data/test/test_helper.rb +14 -0
  69. data/test/view_spec.rb +4 -0
  70. metadata +26 -12
  71. data/assets/init/image_optim.yml +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a5968f3e718b373eb3740d89dd019165130776a2
4
- data.tar.gz: e961a846c6e2d0664a66a1ae5abc9ed791db34dd
3
+ metadata.gz: 6f77f5972f324c7fd1d94e0e62ad167331636c78
4
+ data.tar.gz: 86ce04170afb3b33b76aae7b484eac7247de870d
5
5
  SHA512:
6
- metadata.gz: 068fe33972b2d0f91a9c8be212ac3781392860d518aaf5277104cb3af1e1491766e2f3a5da47bed778bea41273bbd1fba3335c8f0d026845e16d29da85e06311
7
- data.tar.gz: 4f39b31b971373bb1beb2ab1c6689a53bdd73a0e75d3def5bc2eeaae61e83449e7a43581bce9375d2434c2c7e6178a4b912b098459a304080acb11be46664595
6
+ metadata.gz: 78f601288da10bd3db71580b7439c70582a0ec1b9bed2458b9bb79e1e4f6266c8c60cb2c55756fb36809e0ffb003791c150b0a9a010fd375bd4ac9bebd1a5d19
7
+ data.tar.gz: 7e817e4e9e5d38df8950ecc8d390bbfe9f3540703b95149e700b90327ce002fcbbe1a492b94baccff4c9faed55a1efe2b8f8ad5e95f044d8d299f1c0cb880fef
@@ -17,6 +17,8 @@ minify: true
17
17
  # https://inkcite.readme.io/v1.0/docs/image-optimization
18
18
  optimize-images: true
19
19
 
20
+ jpg-quality: 90
21
+
20
22
  # When empty links are found in content, this is the URL that will be
21
23
  # included instead - so that clients understand this link is missing
22
24
  # and needs to be provided.
Binary file
Binary file
Binary file
@@ -27,17 +27,17 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency 'builder'
28
28
  spec.add_dependency 'chunky_png'
29
29
  spec.add_dependency 'erubis'
30
- spec.add_dependency 'faker'
30
+ spec.add_dependency 'faker', '1.7.2'
31
31
  spec.add_dependency 'guard'
32
32
  spec.add_dependency 'guard-livereload'
33
33
  spec.add_dependency 'htmlbeautifier'
34
34
  spec.add_dependency 'image_optim'
35
35
  spec.add_dependency 'image_optim_pack'
36
- spec.add_dependency 'kraken-io'
37
36
  spec.add_dependency 'listen'
38
37
  spec.add_dependency 'litmus'
39
38
  spec.add_dependency 'mail'
40
39
  spec.add_dependency 'mailgun-ruby'
40
+ spec.add_dependency 'mozjpeg'
41
41
  spec.add_dependency 'net-sftp'
42
42
  spec.add_dependency 'rack'
43
43
  spec.add_dependency 'rack-livereload'
@@ -29,6 +29,7 @@ require 'inkcite/util'
29
29
  require 'inkcite/view'
30
30
  require 'inkcite/minifier'
31
31
  require 'inkcite/parser'
32
+ require 'inkcite/post_processor'
32
33
  require 'inkcite/renderer'
33
34
 
34
35
  module Inkcite
@@ -41,20 +41,46 @@ module Inkcite
41
41
  Cli::Init.invoke(name, options)
42
42
  end
43
43
 
44
+ desc 'minify', 'Minify the images in the project'
45
+ option :force,
46
+ :aliases => '-f',
47
+ :desc => 'Force re-optimize every image, not just the updated ones',
48
+ :type => :boolean
49
+ option :image,
50
+ :aliases => '-i',
51
+ :desc => 'Optimize a specific image',
52
+ :type => :string
53
+ def minify
54
+
55
+ if options[:image]
56
+ Image::ImageMinifier.minify(email, options[:image], true)
57
+
58
+ else
59
+ Image::ImageMinifier.minify_all(email, options[:force])
60
+
61
+ original_size = Util.dir_size(email.image_dir)
62
+ compressed_size = Util.dir_size(email.optimized_image_dir)
63
+ compressed_percent = Image::ImageMinifier.compressed_percent(original_size, compressed_size)
64
+
65
+ puts "Compressed from #{Util.pretty_file_size(original_size)} to #{Util.pretty_file_size(compressed_size)} (#{compressed_percent}%)"
66
+ end
67
+
68
+ end
69
+
44
70
  desc 'preview TO [options]', 'Send a preview of the email to a recipient list: developer, internal or client'
45
71
  option :version,
46
72
  :aliases => '-v',
47
73
  :desc => 'Preview a specific version of the email'
48
- option :also,
49
- :aliases => '-a',
50
- :desc => 'Add one or more (space-separated) recipients to this specific mailing',
51
- :type => :array
74
+ option :count,
75
+ :aliases => '-c',
76
+ :desc => 'Override the automatic preview numbering',
77
+ :type => :numeric
52
78
  option :'no-upload',
53
79
  :desc => 'Skip the asset upload, email the preview immediately',
54
80
  :type => :boolean
55
- def preview to=:developer
81
+ def preview list=:developer
56
82
  require_relative 'preview'
57
- Cli::Preview.invoke(email, to, options)
83
+ Cli::Preview.invoke(email, list, options)
58
84
  end
59
85
 
60
86
  desc 'scope [options]', 'Share this email using Litmus Scope (https://litmus.com/scope/)'
@@ -4,47 +4,43 @@ module Inkcite
4
4
  module Cli
5
5
  class Preview
6
6
 
7
- def self.invoke email, to, opt
7
+ def self.invoke email, list, opt
8
8
 
9
9
  # Push the browser preview(s) up to the server to ensure that the
10
10
  # latest images and "view in browser" versions are available.
11
11
  email.upload unless opt[:'no-upload']
12
12
 
13
- also = opt[:also]
14
- unless also.blank?
13
+ # Ensure we're dealing with a symbol rather than string.
14
+ list = list.to_sym
15
15
 
16
- # Sometimes people use commas to separate the --also addresses so
17
- # explode those into an array for convenience. Email is already
18
- # hard enough.
19
- if also.any? { |a| a.match(',') }
20
- also = also.collect { |a| a.split(',') }.flatten
16
+ preview_opt = {}
21
17
 
22
- # Since opt is frozen by Thor we need to make a copy of it in order
23
- # to inject the new array of recipients back into it.
24
- opt = opt.dup
25
- opt[:also] = also
26
-
27
- end
28
- end
29
-
30
- case to.to_sym
18
+ case list
31
19
  when :client
32
- Inkcite::Mailer.client(email, opt)
20
+ preview_opt[:tag] = 'Preview'
21
+ preview_opt[:bcc] = true
33
22
  when :internal
34
- Inkcite::Mailer.internal(email, opt)
23
+ preview_opt[:tag] = 'Internal Preview'
24
+ preview_opt[:bcc] = true
35
25
  when :developer
36
- Inkcite::Mailer.developer(email, opt)
26
+ preview_opt[:tag] = 'Developer Test'
37
27
  else
38
- abort <<-USAGE.strip_heredoc
39
-
40
- Oops! Inkcite doesn't recognize that distribution list. It needs
41
- to be one of 'client', 'internal' or 'developer':
42
-
43
- inkcite preview internal
44
-
45
- USAGE
28
+ preview_opt[:tag] = "#{list.to_s.titleize} Test"
29
+ preview_opt[:bcc] = true
30
+ # abort <<-USAGE.strip_heredoc
31
+ #
32
+ # Oops! Inkcite doesn't recognize that distribution list. It needs
33
+ # to be one of 'client', 'internal' or 'developer':
34
+ #
35
+ # inkcite preview internal
36
+ #
37
+ # USAGE
38
+ #
39
+ # return
46
40
  end
47
41
 
42
+ Mailer.send_to_list email, list, opt.merge(preview_opt)
43
+
48
44
  end
49
45
 
50
46
  end
@@ -11,10 +11,8 @@ module Inkcite
11
11
 
12
12
  def self.start email, opts
13
13
 
14
- puts
15
- puts "Inkcite #{Inkcite::VERSION} is starting up ..."
16
- puts 'Documentation available at http://inkcite.readme.io'
17
- puts
14
+ Util.log "Inkcite #{Inkcite::VERSION} is starting up ..."
15
+ Util.log 'Documentation available at http://inkcite.readme.io'
18
16
 
19
17
  # Read the hostname and port from the opts provided on the command
20
18
  # line - or inherit the default of localhost:4567
@@ -58,10 +56,11 @@ module Inkcite
58
56
  run InkciteApp.new(email, opts)
59
57
  end
60
58
 
61
- puts "Your email is being served at http://#{host}:#{port}"
62
- puts "Point your mobile device to http://#{ip}:#{port}" if ip
63
- puts 'Press CTRL-C to exit server mode'
64
- puts ''
59
+ Util.log ''
60
+ Util.log "Your email is being served at http://#{host}:#{port}"
61
+ Util.log "Point your mobile device to http://#{ip}:#{port}" if ip
62
+ Util.log 'Press CTRL-C to exit server mode'
63
+ Util.log ''
65
64
 
66
65
  begin
67
66
 
@@ -109,11 +108,15 @@ module Inkcite
109
108
  # Minify the image if the source version in images/ is newer
110
109
  # or if the configuration file controlling optimization has
111
110
  # been updated since the last time the image was requested.
112
- Minifier.image(@email, File.basename(path), false) if can_serve(path)
111
+ Image::ImageMinifier.minify(@email, File.basename(path), false) if can_serve(path)
113
112
 
114
113
  # Let the super method handle the actual serving of the image.
115
- super
114
+ res = super
115
+
116
+ # Install cache control into the response. Tried using
117
+ res[1][Rack::CACHE_CONTROL] = NO_CACHE
116
118
 
119
+ res
117
120
  end
118
121
 
119
122
  end
@@ -136,6 +139,11 @@ module Inkcite
136
139
  response = Rack::Response.new
137
140
  response[Rack::CONTENT_TYPE] = 'text/html'
138
141
 
142
+ # Speedup for local development ensuring that a substantial number of reloads
143
+ # (generated when developing multi-version emails simultaneously) can cause
144
+ # the browser to slow down.
145
+ response[Rack::CACHE_CONTROL] = NO_CACHE
146
+
139
147
  begin
140
148
 
141
149
  # Allow the designer to specify both short- and long-form versions of
@@ -146,12 +154,7 @@ module Inkcite
146
154
  format = Util.detect(params['f'], params['format'], @opts[:format])
147
155
  version = Util.detect(params['v'], params['version'], @opts[:version])
148
156
 
149
- # Timestamp all of the messages from this rendering so it is clear which
150
- # messages are associated with this reload.
151
- ts = "[#{Time.now.strftime(DATEFORMAT)}]"
152
-
153
- puts ''
154
- puts "#{ts} Rendering your email [environment=#{environment}, format=#{format}, version=#{version || 'default'}]"
157
+ Util.log "Rendering your email", :environment => environment, :format => format, :version => version || 'default'
155
158
 
156
159
  view = @email.view(environment, format, version)
157
160
 
@@ -163,8 +166,8 @@ module Inkcite
163
166
 
164
167
  unless view.errors.blank?
165
168
  error_count = view.errors.count
166
- puts "#{ts} #{error_count} error#{'s' if error_count > 1} or warning#{'s' if error_count > 1}:"
167
- puts "#{ts} - #{view.errors.join("\n#{ts} - ")}"
169
+ Util.log "#{error_count} error#{'s' if error_count > 1} or warning#{'s' if error_count > 1}:"
170
+ view.errors.each { |e| Util.log(e) }
168
171
  end
169
172
 
170
173
  response.write html
@@ -186,7 +189,7 @@ module Inkcite
186
189
 
187
190
  REQUEST_ROOT = '/'
188
191
 
189
- DATEFORMAT = '%Y-%m-%d %H:%M:%S'
192
+ NO_CACHE = 'no-cache, no-store'
190
193
 
191
194
  end
192
195
  end
@@ -33,7 +33,7 @@ module Inkcite
33
33
  # that the latest images are available.
34
34
  email.upload unless opts[:'no-upload']
35
35
 
36
- Inkcite::Mailer.send(email, opts.merge({ :to => send_to }))
36
+ Inkcite::Mailer.send_test(email, send_to, opts)
37
37
 
38
38
  true
39
39
  end
@@ -24,7 +24,11 @@ module Inkcite
24
24
  end
25
25
 
26
26
  def config
27
- Util.read_yml(File.join(path, 'config.yml'), :fail_if_not_exists => true)
27
+ Util.read_yml(config_file, :fail_if_not_exists => true)
28
+ end
29
+
30
+ def config_file
31
+ File.join(path, 'config.yml')
28
32
  end
29
33
 
30
34
  def formats env=nil
@@ -57,12 +61,12 @@ module Inkcite
57
61
  # Optimizes this email's images if optimize-images is enabled
58
62
  # in the email configuration.
59
63
  def optimize_images
60
- Minifier.images(self, false) if optimize_images?
64
+ Image::ImageMinifier.minify_all(self, false) if optimize_images?
61
65
  end
62
66
 
63
67
  # Optimizes all of the images in this email.
64
68
  def optimize_images!
65
- Minifier.images(self, true)
69
+ Image::ImageMinifier.minify_all(self, true)
66
70
  end
67
71
 
68
72
  def optimize_images?
@@ -72,7 +76,7 @@ module Inkcite
72
76
  # Returns the directory that optimized, compressed images
73
77
  # have been saved to.
74
78
  def optimized_image_dir
75
- File.join(path, optimize_images?? Minifier::IMAGE_CACHE : IMAGES)
79
+ File.join(path, optimize_images?? Image::ImageMinifier::IMAGE_CACHE : IMAGES)
76
80
  end
77
81
 
78
82
  def project_file file
@@ -36,6 +36,10 @@ module Inkcite
36
36
  EASE_IN_OUT = 'ease-in-out'
37
37
  EASE_OUT = 'ease-out'
38
38
 
39
+ # Advanced easing functions courtesy of https://matthewlein.com/ceaser/
40
+ EASE_IN_CUBIC = 'cubic-bezier(0.550, 0.055, 0.675, 0.190)'
41
+ EASE_OUT_QUART = 'cubic-bezier(0.165, 0.840, 0.440, 1.000)'
42
+
39
43
  # Animation name, view context and array of keyframes
40
44
  attr_reader :name, :ctx
41
45
 
@@ -9,11 +9,15 @@ module Inkcite
9
9
  # of 19.9% would render as 25%, 39.9% { ... }
10
10
  attr_accessor :duration
11
11
 
12
+ # Alternative to duration, the ending percentage
13
+ attr_accessor :end_percent
14
+
12
15
  def initialize percent, ctx, styles={}
13
16
 
14
17
  # Animation percents are always rounded to the nearest whole number.
15
- @percent = percent.round(0)
16
- @duration = 0
18
+ @percent = percent
19
+ @end_percent = nil
20
+ @duration = nil
17
21
 
18
22
  # Instantiate a new Style for this percentage.
19
23
  @style = Inkcite::Renderer::Style.new(nil, ctx, styles)
@@ -55,7 +59,10 @@ module Inkcite
55
59
 
56
60
  def to_css prefix
57
61
  css = "#{@percent}%"
58
- css << ", #{@percent + @duration.to_f}%" if @duration > 0
62
+
63
+ ends_at = calc_ends_at
64
+ css << ", #{ends_at}%" if ends_at
65
+
59
66
  css << ' { '
60
67
  css << @style.to_inline_css(prefix)
61
68
  css << ' }'
@@ -64,6 +71,22 @@ module Inkcite
64
71
 
65
72
  private
66
73
 
74
+ # Returns the percent at which the animation keyframe ends
75
+ # based on either end_percent (preferred) or duration (deprecated)
76
+ # being set. Will return nil if neither is present.
77
+ def calc_ends_at
78
+ ends_at = if @end_percent
79
+ @end_percent
80
+ elsif @duration
81
+ (@percent + @duration).round(1)
82
+ end
83
+
84
+ # Ensure that the ending percentage never exceeds 100%
85
+ ends_at = 100 if ends_at && ends_at >= 100.0
86
+
87
+ ends_at
88
+ end
89
+
67
90
  # Creates a copy of the array of styles with the appropriate
68
91
  # properties (e.g. transform) prefixed.
69
92
  def get_prefixed_styles prefix
@@ -0,0 +1,38 @@
1
+ module Inkcite
2
+ module Image
3
+ class ImageMinifier
4
+
5
+ # Base class for all image minifiers in the optimization pipeline
6
+ class Base
7
+
8
+ attr_reader :name
9
+
10
+ def initialize name
11
+ @name = name
12
+ end
13
+
14
+ def minify! email, source_img, cache_img
15
+ raise 'The extending class must implement this method'
16
+ end
17
+
18
+ protected
19
+
20
+ # Common configuration names
21
+ JPG_QUALITY = :'jpg-quality'
22
+
23
+ # JPG quality bounds
24
+ MIN_QUALITY = 0
25
+ MAX_QUALITY = 100
26
+
27
+ def get_jpg_quality config, override_key, default
28
+ quality = (config[override_key] || config[JPG_QUALITY] || default).to_i
29
+ quality = MIN_QUALITY if quality < MIN_QUALITY
30
+ quality = MAX_QUALITY if quality > MAX_QUALITY
31
+ quality
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ module Inkcite
2
+ module Image
3
+ class GuetzliMinifier < ImageMinifier::Base
4
+
5
+ def initialize
6
+ super('Guetzli')
7
+ end
8
+
9
+ def minify! email, source_img, cache_img
10
+
11
+ # Grab the full path to the guetzli binary
12
+ guetzli_path = `which guetzli`.delete("\n")
13
+ unless guetzli_path.blank?
14
+
15
+ cmd = []
16
+ cmd << guetzli_path
17
+
18
+ config = email.config
19
+
20
+ quality = get_jpg_quality(config, GUETZLI_QUALITY, DEFAULT_QUALITY)
21
+ if quality > 0 && quality < MAX_QUALITY
22
+
23
+ # Per the Guetzli documentation, a value less than 84 isn't useful.
24
+ quality = MIN_GUETZLI_QUALITY if quality < MIN_GUETZLI_QUALITY
25
+ cmd << "--quality #{quality}"
26
+
27
+ end
28
+
29
+ cmd << %Q("#{source_img}")
30
+ cmd << %Q("#{cache_img}")
31
+
32
+ Util::exec(cmd)
33
+
34
+ true
35
+
36
+ else
37
+
38
+ # No guetzli, so simply move the source image into the destination
39
+ # position without compression.
40
+ FileUtils.copy(source_img, cache_img)
41
+
42
+ false
43
+ end
44
+
45
+
46
+ end
47
+
48
+ private
49
+
50
+ # Default quality is zero - which lets Guetzli decide how best to
51
+ # optimize the image.
52
+ DEFAULT_QUALITY = 0
53
+
54
+ # The minimum quality setting per the Guetzli runtime.
55
+ MIN_GUETZLI_QUALITY = 84
56
+
57
+ # Configuration field names
58
+ GUETZLI_QUALITY = :'guetzli-quality'
59
+
60
+ end
61
+ end
62
+ end