inkcite 1.14.0 → 1.15.0

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 (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