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
@@ -104,9 +104,29 @@ module Inkcite
104
104
  # appear in the {footnotes} rendering.
105
105
  instance.active = true
106
106
 
107
+ unless id.blank?
108
+
109
+ # Check to see if the once flag is enabled. If enabled, check to see if
110
+ # this footnote has appeared before.
111
+ once = opt[:once]
112
+ return '' if once && !ctx.once?("#{id}-footnote")
113
+
114
+ end
115
+
107
116
  # Allow footnotes to be defined without showing a symbol
108
117
  hidden = opt[:hidden] || (opt[:hidden] == '1')
109
- "#{instance.symbol}" unless hidden
118
+ return '' if hidden
119
+
120
+ # Check to see if the footnote should be automatically surrounded
121
+ # by the superscript helper.
122
+ sup = opt[:sup] && !ctx.text?
123
+
124
+ html = ''
125
+ html << '{sup}' if sup
126
+ html << instance.symbol
127
+ html << '{/sup}' if sup
128
+
129
+ html
110
130
  end
111
131
 
112
132
  end
@@ -125,7 +145,7 @@ module Inkcite
125
145
  # on the format of the email.
126
146
  tmpl = opt[:tmpl] || opt[:template]
127
147
  if tmpl.blank?
128
- tmpl = ctx.text? ? "($symbol$) $text$\n\n" : "<sup>$symbol$</sup> $text$<br><br>"
148
+ tmpl = ctx.text? ? '($symbol$) $text$\n\n' : '<sup>$symbol$</sup> $text$<br><br>'
129
149
 
130
150
  elsif ctx.text?
131
151
 
@@ -11,6 +11,7 @@ module Inkcite
11
11
 
12
12
  mix_background img, opt, ctx
13
13
  mix_border img, opt, ctx
14
+ mix_margins img, opt, ctx
14
15
 
15
16
  # Check to see if there is alt text specified for this image. We are
16
17
  # testing against nil because sometimes the author desires an empty
@@ -66,12 +67,22 @@ module Inkcite
66
67
  # vertically aligns the image with the text.
67
68
  inline = (display == INLINE)
68
69
 
70
+ # Allow max-height to be specified.
71
+ max_height = opt[MAX_HEIGHT]
72
+ img.style[MAX_HEIGHT] = px(max_height) unless max_height.blank?
73
+
69
74
  align = opt[:align] || ('absmiddle' if inline)
70
75
  img[:align] = align unless align.blank?
71
76
 
72
77
  valign = opt[:valign] || ('middle' if inline)
73
78
  img.style[VERTICAL_ALIGN] = valign unless valign.blank?
74
79
 
80
+ # Fix for unexpected whitespace underneath images when emails
81
+ # are viewed in Outlook.com. Thanks to @HTeuMeuLeu
82
+ # https://emails.hteumeuleu.com/outlook-coms-latest-bug-and-how-to-fix-gaps-under-images-ee1816671461
83
+ id = ctx.unique_id :img
84
+ img[:id] = quote("OWATemporaryImageDivContainer#{id}")
85
+
75
86
  html = ''
76
87
 
77
88
  # Check to see if an outlook-specific image source has been
@@ -4,10 +4,6 @@ module Inkcite
4
4
 
5
5
  protected
6
6
 
7
- # Name of the property that allows an outlook-specific src to be specified
8
- # for an image.
9
- OUTLOOK_SRC = :'outlook-src'
10
-
11
7
  # Display mode constants
12
8
  BLOCK = 'block'
13
9
  DEFAULT = 'default'
@@ -96,11 +92,12 @@ module Inkcite
96
92
  end
97
93
 
98
94
  def missing_dimensions? att
99
- DIMENSIONS.any? { |dim| att[dim].to_i <= 0 }
95
+ DIMENSIONS.any? { |dim| att[dim].blank? }
100
96
  end
101
97
 
102
98
  def mix_dimensions img, opt, ctx
103
- DIMENSIONS.each { |dim| img[dim] = opt[dim].to_i }
99
+ super
100
+ DIMENSIONS.each { |dim| img[dim] = opt[dim] }
104
101
  end
105
102
 
106
103
  private
@@ -15,6 +15,10 @@ module Inkcite
15
15
  # Make sure we're converting any embedded values in the host URL
16
16
  url = Renderer.render(url, browser_view)
17
17
 
18
+ # Cache-bust the URL to ensure recipients see the most recent version of
19
+ # the uploaded HTML
20
+ Util::add_query_param(url, Time.now.to_i) if !ctx.production? && ctx.is_enabled?(Email::CACHE_BUST)
21
+
18
22
  # Optional link override color.
19
23
  color = opt[:color]
20
24
 
@@ -41,7 +41,12 @@ module Inkcite
41
41
  # including font, background color, border, etc.
42
42
  mix_all a, opt, ctx
43
43
 
44
- id, href, target_blank = Link.process(opt[:id], opt[:href], opt[:force], ctx)
44
+ # If no-tag is specified, override the default tagging
45
+ # behavior. Useful when the tagging interferes with
46
+ # behavior on dynamic pages.
47
+ add_tag = opt[:'no-tag'] != true
48
+
49
+ id, href, target_blank = Link.process(opt[:id], opt[:href], opt[:force], ctx, add_tag)
45
50
 
46
51
  a[:target] = BLANK if target_blank
47
52
 
@@ -100,7 +105,7 @@ module Inkcite
100
105
  html
101
106
  end
102
107
 
103
- def self.process id, href, force, ctx
108
+ def self.process id, href, force, ctx, add_tag=true
104
109
 
105
110
  # Initially assume a blank target isn't needed
106
111
  target_blank = false
@@ -163,7 +168,7 @@ module Inkcite
163
168
  ctx.error('Link href appears to be invalid', { :id => id, :href => href }) unless force || valid?(href)
164
169
 
165
170
  # Optionally tag the link's query string for post-send log analytics.
166
- href = add_tagging(id, href, ctx)
171
+ href = add_tagging(id, href, ctx) if add_tag
167
172
 
168
173
  if last_href.blank?
169
174
 
@@ -189,7 +194,7 @@ module Inkcite
189
194
 
190
195
  end
191
196
 
192
- [ id, href, target_blank ]
197
+ [id, href, target_blank]
193
198
  end
194
199
 
195
200
  private
@@ -220,16 +225,38 @@ module Inkcite
220
225
  def self.add_tagging id, href, ctx
221
226
 
222
227
  # Check to see if we're tagging links.
223
- tag = ctx[TAG_LINKS]
224
- unless tag.blank?
225
-
226
- # Blank tag domain means tag all the links - otherwise, make sure the
227
- # href matches the desired domain name.
228
- tag_domain = ctx[TAG_LINKS_DOMAIN]
229
- if tag_domain.blank? || href =~ /^https?:\/\/[^\/]*#{tag_domain}/
230
- Util::add_query_param(href, replace_tag(tag, id, ctx))
228
+ tag_map = ctx[TAG_LINKS]
229
+ unless tag_map.blank?
230
+
231
+ # This is the tag to be applied.
232
+ tag = nil
233
+
234
+ # Support for a map of tags where the matching domain is
235
+ # the key and the value is the tagging.
236
+ if tag_map.is_a?(Hash)
237
+ longest_domain = ''
238
+
239
+ tag_map.each do |domain, _tag|
240
+ if href =~ /^https?:\/\/[^\/]*#{domain}/
241
+ if domain.length > longest_domain.length
242
+ longest_domain = domain
243
+ tag = _tag
244
+ end
245
+ end
246
+ end
247
+
248
+ else
249
+
250
+ # Blank tag domain means tag all the links - otherwise, make sure the
251
+ # href matches the desired domain name.
252
+ tag_domain = ctx[TAG_LINKS_DOMAIN]
253
+ tag = tag_map if tag_domain.blank? || href =~ /^https?:\/\/[^\/]*#{tag_domain}/
254
+
231
255
  end
232
256
 
257
+ # If a tag was identified then apply it to the href.
258
+ href = Util::add_query_param(href, replace_tag(tag, id, ctx)) unless tag.blank?
259
+
233
260
  end
234
261
 
235
262
  href
@@ -16,7 +16,7 @@ module Inkcite
16
16
 
17
17
  mix_background img, opt, ctx
18
18
 
19
- mix_margins img, opt, ctx
19
+ mix_margins img, opt, ctx, false
20
20
 
21
21
  display = opt[:display]
22
22
  img.style[:display] = "#{display}" if display && display != BLOCK && display != DEFAULT
@@ -29,6 +29,7 @@ module Inkcite
29
29
 
30
30
  # Other mobile-specific properties
31
31
  MOBILE_HEIGHT = :'mobile-height'
32
+ MOBILE_MAX_WIDTH = :'mobile-max-width'
32
33
  MOBILE_PADDING = :'mobile-padding'
33
34
  MOBILE_WIDTH = :'mobile-width'
34
35
 
@@ -209,6 +210,13 @@ module Inkcite
209
210
  mix_directional element, element.mobile_style, opt, ctx, MOBILE_BORDER, :border
210
211
  end
211
212
 
213
+ def mix_dimensions element, opt, ctx
214
+
215
+ max_width = opt[MOBILE_MAX_WIDTH]
216
+ element.mobile_style[MAX_WIDTH] = px(max_width) unless max_width.blank?
217
+
218
+ end
219
+
212
220
  def mix_font element, opt, ctx, parent=nil
213
221
 
214
222
  # Let the super class do its thing and grab the name of the font
@@ -237,7 +245,7 @@ module Inkcite
237
245
  font
238
246
  end
239
247
 
240
- def mix_margins element, opt, ctx
248
+ def mix_margins element, opt, ctx, outlookCompatible=true
241
249
  super
242
250
  mix_directional element, element.mobile_style, opt, ctx, MOBILE_MARGIN, :margin, true
243
251
  end
@@ -43,7 +43,35 @@ module Inkcite
43
43
  protected
44
44
 
45
45
  def get_share_href url, text, opts, ctx
46
- %Q(https://twitter.com/share?url=#{url}&text=#{text})
46
+
47
+ params = {}
48
+ params[:url] = url unless url.blank?
49
+ params[:text] = text
50
+ params[:hashtags] = opts[:hashtags]
51
+
52
+ val = "https://twitter.com/intent/tweet?"
53
+ val << Renderer.join_hash(params, '=', '&')
54
+ val
55
+ end
56
+
57
+ end
58
+
59
+ class Instagram < Social
60
+
61
+ def initialize
62
+ super(:src => 'instagram.png', :alt => 'Instagram', :cta => 'Post')
63
+ end
64
+
65
+ protected
66
+
67
+ def get_share_href url, text, opts, ctx
68
+
69
+ val = "https://www.instagram.com"
70
+
71
+ username = opts[:username]
72
+ val << "/#{username}" unless username.blank?
73
+
74
+ val
47
75
  end
48
76
 
49
77
  end
@@ -62,10 +90,10 @@ module Inkcite
62
90
  id = opts[:id]
63
91
 
64
92
  share_url = opts.delete(:href).to_s
65
- ctx.error("Social sharing 'href' attribute can't be blank", :tag => tag, :id => id) if share_url.blank?
93
+ #ctx.error("Social sharing 'href' attribute can't be blank", :tag => tag, :id => id) if share_url.blank?
66
94
 
67
95
  share_text = opts.delete(:text).to_s
68
- ctx.error("Social sharing 'text' attribute can't be blank", :tag => tag, :id => id, :href => share_url) if share_text.blank?
96
+ #ctx.error("Social sharing 'text' attribute can't be blank", :tag => tag, :id => id, :href => share_url) if share_text.blank?
69
97
 
70
98
  # Let the extending class format the fully-qualified URL to the sharing service.
71
99
  service_href = get_share_href Util.escape(share_url), Util.escape(share_text), opts, ctx
@@ -165,18 +165,17 @@ module Inkcite
165
165
  (min_speed..max_speed)
166
166
  end
167
167
 
168
+ # An array of source images to be applied to the children or
169
+ # an empty array if there are none.
168
170
  def src
169
- return @src if defined?(@src)
171
+ @src ||= begin
170
172
 
171
- # Check to see if a source image has been specified for the snowflakes.
172
- @src = @opt[:src]
173
+ # Check to see if a source image has been specified for the snowflakes.
174
+ # Split on a comma to treat the source listing as an array. Then verify
175
+ # that each of the referenced images exists
176
+ @opt[:src].to_s.split(',').select { |img| @ctx.assert_image_exists(img) }
173
177
 
174
- # Release the image name if one has been provided but doesn't exist in
175
- # the project - this will cause the special effect to default to the
176
- # non-image default behavior.
177
- @src = nil if @src && !@ctx.assert_image_exists(@src)
178
-
179
- @src
178
+ end
180
179
  end
181
180
 
182
181
  def position_range
@@ -304,9 +303,8 @@ module Inkcite
304
303
  OPACITY_MAX = :'max-opacity'
305
304
  OPACITY_CEIL = 1.0
306
305
 
307
- # Static constants for animation-specific CSS
308
- ANIMATION_DELAY = :'animation-delay'
309
- ANIMATION_DURATION = :'animation-duration'
306
+ # For converting degrees to radians and back
307
+ PI_OVER_180 = Math::PI / 180.0
310
308
 
311
309
  # The extending class can override this method to perform any
312
310
  # additional configuration on the style that affects all
@@ -361,7 +359,7 @@ module Inkcite
361
359
  color = sfx.color
362
360
  style[BACKGROUND_COLOR] = color unless none?(color)
363
361
  else
364
- style[BACKGROUND_IMAGE] = "url(#{sfx.ctx.image_url(src)})"
362
+ style[BACKGROUND_IMAGE] = "url(#{sfx.ctx.image_url(src.first)})" unless src.length > 1
365
363
  style[BACKGROUND_SIZE] = '100%'
366
364
  end
367
365
 
@@ -377,6 +375,10 @@ module Inkcite
377
375
  # CSS classes to them allowing each to be sized, animated uniquely.
378
376
  def create_child_elements html, styles, sfx
379
377
 
378
+ # Get a local handle on the src array in case we need to apply
379
+ # a random one to each child (because there are multiple)
380
+ src = sfx.src
381
+
380
382
  sfx.count.times do |n|
381
383
 
382
384
  child_class_name = sfx.child_class_name(n)
@@ -387,6 +389,13 @@ module Inkcite
387
389
  # This is the custom style to be applied to the child.
388
390
  style = Inkcite::Renderer::Style.new(".#{child_class_name}", sfx.ctx)
389
391
 
392
+ # If there are multiple images, apply the next image based on the
393
+ # child's index.
394
+ if src.length > 1
395
+ src_index = n % src.length
396
+ style[BACKGROUND_IMAGE] = "url(#{sfx.ctx.image_url(src[src_index])})"
397
+ end
398
+
390
399
  # This is the animation declaration (timing, duration, etc.) for this child.
391
400
  animation = Inkcite::Animation.new(sfx.animation_class_name(n), sfx.ctx)
392
401
 
@@ -0,0 +1,32 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Sup < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ html = ''
8
+
9
+ if tag == '/sup'
10
+ html << '</sup>'
11
+
12
+ else
13
+
14
+ sup = Element.new('sup', :style => { VERTICAL_ALIGN => :top })
15
+
16
+ font_size = (opt[FONT_SIZE] || 10).to_i
17
+ sup.style[FONT_SIZE] = px(font_size)
18
+
19
+ line_height = (opt[LINE_HEIGHT] || 10).to_i
20
+ sup.style[LINE_HEIGHT] = px(line_height)
21
+
22
+ html << sup.to_s
23
+
24
+ end
25
+
26
+ html
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
@@ -28,6 +28,9 @@ module Inkcite
28
28
  # css isn't supported.
29
29
  element[:bgcolor] = bgcolor unless bgcolor.nil?
30
30
 
31
+ # Allow background gradients on tables and tds.
32
+ mix_background_gradient element, opt, ctx
33
+
31
34
  bgimage = opt[:background]
32
35
  bgposition = opt[BACKGROUND_POSITION]
33
36
  bgrepeat = opt[BACKGROUND_REPEAT]
@@ -0,0 +1,76 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Topic < Base
4
+
5
+ class Instance
6
+
7
+ # Name of the topic as it will appear in the topic list.
8
+ attr_reader :name
9
+
10
+ # Order of the topic, higher wins
11
+ attr_reader :priority
12
+
13
+ def initialize name, priority
14
+ @name = name
15
+ @priority = priority
16
+ end
17
+
18
+ end
19
+
20
+ def render tag, opt, ctx
21
+
22
+ name = opt[:name]
23
+
24
+ if name.blank?
25
+ ctx.error 'Every topic must have a name'
26
+
27
+ else
28
+
29
+ # Initialize the array of topic instances that live in the
30
+ # View's arbitrary data holder.
31
+ ctx.data[:topics] ||= []
32
+
33
+ # Push a topic instance onto the list
34
+ ctx.data[:topics] << Instance.new(name, opt[:priority].to_i)
35
+
36
+ end
37
+
38
+ nil
39
+ end
40
+
41
+ end
42
+
43
+ class TopicList < Base
44
+
45
+ include PostProcessor
46
+
47
+ def post_process html, ctx
48
+
49
+ topics = ctx.data[:topics]
50
+ if topics.blank?
51
+ ctx.error '{topic-list} included but no topics defined'
52
+
53
+ else
54
+
55
+ # Sort the topics highest priority first.
56
+ sorted_topics = topics.sort { |lhs, rhs| rhs.priority <=> lhs.priority }.collect(&:name).join(', ')
57
+ html.gsub!(TOC_INTERMEDIARY, sorted_topics)
58
+
59
+ end
60
+
61
+ html
62
+ end
63
+
64
+ def render tag, opt, ctx
65
+ ctx.post_processors << self
66
+ TOC_INTERMEDIARY
67
+ end
68
+
69
+ private
70
+
71
+ TOC_INTERMEDIARY = '%%TOPIC-LIST-PLACEHOLDER%%'
72
+
73
+ end
74
+
75
+ end
76
+ end